Open Food Facts Omi 集成:为 Omi 聊天添加免登录的食品信息查询工具
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
导读
本文以仓库中plugins/omi-openfoodfacts-app/目录下的 README.md 为核心,系统讲解该独立部署的 Omi 插件服务如何基于 Open Food Facts 公开只读 API,为 Omi 聊天提供按名称搜索食品、按条形码查询、多商品对比与过敏原核查四大能力。读完本文,你将掌握该插件的工具清单与 Omi 应用配置方式、四个聊天工具的请求/响应契约、全部环境变量语义、本地开发与 Railway 部署流程,并能结合 main.py 源码理解其数据字段选取、营养数据口径、搜索端点选择与过敏原匹配的底层实现细节。
一、插件定位:Omi 生态中的轻量无账号查询服务
在 Omi 插件体系中,omi-*-app/是一批相互独立部署的 FastAPI 服务,每个目录都自带main.py、依赖文件与部署描述(Dockerfile / Procfile / railway.toml),互不依赖、独立上线(见 plugins/README.md)。omi-openfoodfacts-app正是其中之一,它的独特之处在于:
- 零账号接入:完全基于 Open Food Facts 的只读 API,不要求用户连接任何账号,不需要 OAuth,也不需要 API Key;
- 对话内直用:通过 Omi 的 Chat Tools 机制,用户在聊天中直接发问即可触发查询;
- 食品领域专精:聚焦包装食品的配料、营养、评分与过敏原信息,返回结构化的产品摘要。
与需要安装 omi-plugin-sdk 的 webhook 型插件不同,本插件是“独立部署的 chat tools 服务”,通过暴露.well-known/omi-tools.json清单向 Omi 声明能力,属于插件体系中“独立部署服务”这一类。
二、功能总览:五类开箱即用的食品查询能力
依据 README,该插件提供以下能力,全部由只读 Open Food Facts API 支撑:
- 按名称搜索包装食品:例如搜索“燕麦奶”;
- 按条形码查询单个产品:读取包装上的 EAN 码即可命中数据库记录;
- 最多五个条形码的产品对比:横向比较营养、评分与标签;
- 过敏原核查:将某条形码(或搜索结果第一条)中的过敏原、致敏痕迹与配料文本,与用户想要避开的食物词表进行匹配;
- 返回结构化营养与评级信息:每 100g 营养数据、Nutri-Score、NOVA 加工分级、Eco-Score、标签、类别、配料文本以及产品正面小图(若存在)。
全部调用均为只读请求,无需 OAuth 或 API Key——这是该项目最重要的集成前提,意味着部署成本极低,用户可以即刻在对话中体验。
三、Omi 应用配置:三行填表即可接入
在 Omi 平台创建应用时,按 README 给出的字段填写:
| 字段 | 取值 |
|---|---|
| Chat Tools Manifest URL | https://YOUR-APP.up.railway.app/.well-known/omi-tools.json |
| Setup URL | 留空 |
| Setup Completed URL | 留空 |
其中的YOUR-APP需替换为你实际部署的 Railway 服务域名。由于本插件无需账号绑定,因此两个 Setup 相关 URL 都保持空白,Omi 在识别到工具清单后即可直接调用。
/ .well-known/omi-tools.json这个路径由 main.py 中的get_omi_tools_manifest()直接返回(同时提供了/manifest.json别名端点),清单内每个工具都声明了名称、描述、HTTP 端点、方法、参数 schema、auth_required: False与status_message,这正是 Omi 客户端生成工具调用所需的最小契约。
四、Chat Tools 清单:四个工具与端点映射
README 给出了四个工具的完整映射:
| 工具 | 端点 | 用途 |
|---|---|---|
search_foods | POST /tools/search_foods | 按产品名称搜索 |
lookup_barcode | POST /tools/lookup_barcode | 查询单个条形码 |
compare_foods | POST /tools/compare_foods | 最多对比五个条形码 |
check_allergens | POST /tools/check_allergens | 对某条形码或首个搜索结果核查需避开的过敏原 |
从 main.py 源码看,四个端点都遵循统一的 FastAPI 实现模式:先解析 JSON 请求体(_json_body),参数不合法时返回success=False的ChatToolResponse;成功后统一返回{success, message, data}结构。各工具的参数契约如下:
search_foods:query(必填,字符串)+page_size(可选整数,默认 5,上限 10);lookup_barcode:barcode(必填,字符串,可含包装上的数字或整串条形码);compare_foods:barcodes(必填,字符串数组,最多取前 5 个);check_allergens:avoid(必填,字符串数组,如["milk", "peanuts", "gluten"]),外加可选的barcode或query用于定位产品。
典型对话示例
README 提供了四条可直接体验的示例提示词:
- “Look up barcode 737628064502.”
- “Find oat milk and show sugar per 100g.”
- “Compare these cereal barcodes.”
- “Does this snack list milk, peanuts, or gluten?”
这些提示词分别对应lookup_barcode、search_foods、compare_foods与check_allergens,Omi 会根据工具描述自动路由到对应端点。
五、源码级原理:数据口径与实现细节
5.1 字段白名单:PRODUCT_FIELDS
main.py 中定义了一个固定字段白名单PRODUCT_FIELDS,包括code、product_name、generic_name、brands、quantity、categories_tags、labels_tags、ingredients_text、allergens_tags、traces_tags、nutriscore_grade、nova_group、ecoscore_grade、nutriments、image_front_small_url。搜索与条形码查询都会通过fields参数把该白名单传给 Open Food Facts API,既缩小了响应体积,也避免把无关大字段灌进聊天上下文。
5.2 营养口径:只认每 100g 数值
_summarize_product()输出的nutrition_per_100g只包含能量(kcal)、脂肪、饱和脂肪、碳水化合物、糖、膳食纤维、蛋白质与盐这 8 个维度。关键在于_nutrient()的实现:它只读取带_100g后缀的键(如fat_100g),并明确拒绝回退到无后缀键——因为无后缀的nutriments键的基准可能是“每份”,若将其误标为 per-100g 就是错误数据。这一口径在 test_nutrient_basis.py 中有专门测试守护:当某产品只有fat(每份)而无fat_100g时,nutrition_per_100g.fat_g必须返回None而非泄漏的每份数值。
5.3 搜索端点:为什么用/cgi/search.pl
源码注释明确写道:/cgi/search.pl才是 Open Food Facts 的全文产品搜索端点,而/api/v2/search不是。因此_search_foods()调用GET /cgi/search.pl,携带action=process、search_terms、search_simple=1、json=1、page_size与fields参数。test_search_endpoint.py 通过 monkeypatch 捕获实际路径与参数,断言请求必须落在/cgi/search.pl且json=1,从测试层面固定了这一契约。
5.4 过敏原匹配:否定词的“反误报”处理
check_allergens的匹配逻辑在_ingredient_mentions_term()中实现,它对配料文本做小写归一化后使用词边界正则匹配,并专门处理两类否定表达以避免误报:
- 跳过
-free/free后缀(如gluten-free不命中 gluten); - 跳过
no/non-前缀(如no peanuts、non-dairy不命中相应词)。
最终返回的data中包含matches(命中的回避词)、checked_sources(["allergens", "traces", "ingredients text"])与data_note,并生成可读消息,如"{name} may include: milk, peanuts.";未命中时则提示用户仍需核对包装标签。
5.5 网络层:超时、UA 与容错
所有上游请求都通过_openfoodfacts_get()发出:设置Accept: application/json与自定义 User-Agent,超时默认 8 秒(由环境变量控制);遇到网络异常或非 JSON 响应时返回{"error": ...},而不是抛出异常,保证聊天工具稳定返回结构化失败信息。为避免阻塞事件循环,异步端点经run_in_threadpool把同步 requests 调用放入线程池执行。
六、环境变量:全部可调项一览
| 变量 | 默认值 | 说明 |
|---|---|---|
OPENFOODFACTS_BASE_URL | https://world.openfoodfacts.org | 生产环境 API 根地址;staging 可切换为https://world.openfoodfacts.net |
OPENFOODFACTS_USER_AGENT | OmiOpenFoodFactsApp/1.0(附项目仓库地址作为身份标识) | Open Food Facts 要求应用自报身份,便于数据贡献者联系 |
OPENFOODFACTS_TIMEOUT_SECONDS | 8 | 每个上游请求的超时秒数,可调大以适配慢网络 |
PORT | 8080 | 服务监听端口,Railway 与本地运行均使用 |
在 main.py 中,前三项在模块加载时通过os.getenv读取,BASE_URL还会执行rstrip("/")以兼容带尾斜杠的配置值;PORT则用于uvicorn.run的端口绑定。需要注意:切换BASE_URL到 staging 域时,/cgi/search.pl与/api/v2/product/{code}.json的相对路径均保持不变,只是根域名不同。
七、本地开发与验证
README 给出的本地启动方式为:
pip install -r requirements.txt uvicorn main:app --reload --port 8080依赖锁定在 requirements.txt:fastapi==0.104.1、uvicorn==0.24.0、requests==2.34.2、pydantic==2.5.2。服务启动后,打开:
http://localhost:8080/.well-known/omi-tools.json即可检查工具清单是否正常返回。此外还可验证:
curl http://localhost:8080/health健康检查端点返回{"status": "ok", "service": "omi-openfoodfacts-app"}。本地调试单个工具时,例如搜索“燕麦奶”:
curl -X POST http://localhost:8080/tools/search_foods \ -H "Content-Type: application/json" \ -d '{"query": "oat milk", "page_size": 3}'响应遵循ChatToolResponse结构,形如:
{ "success": true, "message": "Found 3 product(s) for 'oat milk'.", "data": { "query": "oat milk", "count": 42, "products": [ { "barcode": "737628064502", "name": "Oat Milk", "brands": "ExampleBrand", "quantity": "1 L", "nutri_score": "B", "nova_group": "2", "eco_score": "A", "allergens": ["gluten"], "traces": ["sesame seeds"], "labels": ["vegan", "organic"], "categories": ["plant milks", "oat milks"], "ingredients": "Water, oats...", "nutrition_per_100g": { "energy_kcal": 48, "fat_g": 1.5, "saturated_fat_g": 0.2, "carbohydrates_g": 7.0, "sugars_g": 4.0, "fiber_g": 0.8, "proteins_g": 1.0, "salt_g": 0.1 }, "image_url": "https://.../front_small.jpg", "data_note": "Open Food Facts data is community contributed and can be incomplete." } ], "data_note": "Search is capped by this app to reduce API load." } }八、部署到 Railway
仓库已为 Railway 一键部署备好全部配置:
- railway.toml:使用
nixpacks构建;启动命令为uvicorn main:app --host 0.0.0.0 --port $PORT;健康检查指向/health(超时 100 秒);失败时自动重启,最多重试 3 次; - Procfile:
web: uvicorn main:app --host 0.0.0.0 --port $PORT,兼容依赖 Procfile 的平台; - runtime.txt:指定 Python 版本
python-3.11。
部署成功后,将railway.toml中的启动命令与 README 中 Omi App 配置表里的Chat Tools Manifest URL对应起来——把YOUR-APP.up.railway.app换成真实域名,填入 Omi 控制台即可完成接入。
九、数据边界与防滥用设计(重要提示)
README 明确强调两点数据注意,这也是使用本插件时必须传达给用户的边界:
社区贡献数据的天然局限:Open Food Facts 是社区贡献数据库,某产品缺失过敏原、营养字段或配料清单,并不证明该产品安全或完整。因此插件在工具响应中内置了
data_note提示(如"Open Food Facts data is community contributed and can be incomplete."、"Missing Open Food Facts allergen data does not prove the food is safe."),避免 Omi 把不完整数据包装成“保证”。搜索限流:搜索工具将
page_size上限固定在 10,防止聊天使用演变成高并发搜索客户端。此外 main.py 的_safe_int()对page_size做了1~10的钳制,compare_foods的_collect_foods_from_body()也只遍历barcodes[:5],从源头限制了单次请求对上游的冲击。
十、小结
omi-openfoodfacts-app是 Omi 插件体系“独立部署 chat tools 服务”的典型范例:以一份.well-known/omi-tools.json清单声明四个只读工具,用不超过 10 个环境变量的配置即可完成从本地到生产的一致性运行。它同时示范了两个值得复用的工程实践——只取_100g营养键的严格数据口径与带否定词过滤的配料匹配算法,两者均有仓库内测试用例背书,可作为后续任何面向消费者数据的聊天工具的设计参考。更多插件体系说明可参见 plugins/README.md,插件共享模型定义见 omi-plugin-sdk/README.md。
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考