Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证The 14 EU allergens on every dish in EN/RU/TR/DE; nothing is printed before a chef confirms it (Regulation (EU) No 1169/2011).
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗▶ Live demo in your browser · Case study · Part of Daniil Dushin's portfolio
Marks the 14 allergens that EU food law requires to be declared on every dish of a restaurant or hotel kitchen, and prints the menu with allergen numbers in English, Russian, Turkish and German. It reads the ingredient lists with a curated multilingual dictionary, asks an LLM about what it cannot settle, and publishes nothing until a person confirms each dish.
This tool assists the kitchen. It does not replace the business's own allergen management, supplier specifications or staff training, and it is not legal advice. How allergen information must be shown is decided by each country (see below).
Kitchens change recipes often: a new supplier's stock cube, pesto made with cashews instead of pine nuts, a sauce finished with butter this season. The allergen information on the menu is written once and goes stale. In Antalya, Spain or Germany the same menu is read by tourists in several languages, and a guest with a nut allergy has to trust a number next to a dish name.
What the law asks for (checked against the consolidated text of Regulation (EU) No 1169/2011 of 1 January 2018, as published on EUR-Lex by the EU Publications Office):
Sources: Regulation 1169/2011 on EUR-Lex, consolidated text 2018-01-01, Delegated Regulation 78/2014, Turkish regulation annexes (Resmî Gazete).
pesto (basil, cashews, parmesan) takes the allergens
from the listed parts instead of the guess for "pesto".uv run python -m app check "spaghetti, egg yolk, pecorino, guanciale".Real screenshots of the demo (uv run python -m app --demo), taken with headless Edge.
| A dish whose recipe changed | The printed menu in German | The same menu in Turkish |
|---|---|---|
flowchart LR
R[Recipe text<br/>EN / RU / TR / DE] --> P[Parse lines<br/>and brackets]
P --> F[Fold: case,<br/>diacritics, İ/ı, ß, ё]
F --> M[Dictionary patterns<br/>2,429 phrases]
M --> L[Longest non-overlapping<br/>matches]
L --> A{Per ingredient}
A -->|all words known| OK[found / none]
A -->|unknown, ambiguous,<br/>possible, free-from| C[check]
C -->|key set| AI[Claude: structured<br/>suggestion + reason]
C -->|demo / no key| H[flagged for the chef]
OK --> FORM[Confirmation form<br/>pre-selected]
AI --> FORM
H --> FORM
FORM -->|name + tick boxes| DB[(SQLite<br/>append-only log)]
DB --> MENU[Printed menu<br/>+ JSON export]
A dish's life:
stateDiagram-v2
[*] --> NotConfirmed: dish created
NotConfirmed --> Confirmed: a person confirms (name logged)
Confirmed --> RecipeChanged: ingredients edited
RecipeChanged --> Confirmed: a person confirms again
Confirmed --> Review: dictionary finds an allergen not confirmed
Review --> Confirmed: confirmed again
note right of Confirmed: only this state is printed
Reordering ingredients, changing letter case or spacing, or renaming the dish does not reset the confirmation (the recipe hash is order- and case-independent). Changing back to an old recipe does not bring the old confirmation back.
Requires uv (it installs Python 3.12 itself).
cp .env.example .env
uv sync
uv run python -m app --demo
Open http://127.0.0.1:8110. Three sample dishes wait for confirmation: an İskender kebab
and a carbonara with ingredients the dictionary does not know (döner, guanciale), and
a satay whose recipe was changed after it was confirmed. With Docker:
cp .env.example .env && docker compose up --build.
For the AI suggestions, set ANTHROPIC_API_KEY in .env. For real dishes, set
DEMO_MODE=false (the sample menu lives in its own data/demo.db).
$ uv run python -m app check "spaghetti, egg yolk, pecorino, guanciale"
Ingredients: spaghetti, egg yolk, pecorino, guanciale
spaghetti found 1
1 Gluten <- 'spaghetti'
egg yolk found 3
3 Eggs <- 'egg yolk'
pecorino found 7
7 Milk <- 'pecorino'
guanciale CHECK -
not in the dictionary: guanciale
Allergens: 1 Gluten · 3 Eggs · 7 Milk
A person must check 1: guanciale
'?' = possible, not certain. This tool assists; a person confirms the dish.
Options: --lang ru|tr|de for allergen names, --json, --ai (asks the AI about the
items to check), - reads the list from stdin.
See .env.example.
| Variable | Default | Meaning |
|---|---|---|
DEMO_MODE | true in the example | Sample menu of 16 dishes in data/demo.db |
ANTHROPIC_API_KEY | empty | Enables AI suggestions; empty = flag for the chef |
ANTHROPIC_MODEL | claude-opus-5-5 | Model for the suggestions |
ANTHROPIC_EFFORT | medium | low … max |
AI_SUGGESTIONS | true | false = never call the AI |
RESTAURANT_NAME | empty | Title of the printed menu |
DATABASE_PATH | empty | data/allergens.db (data/demo.db in demo mode) |
TIMEZONE | Europe/Istanbul | For confirmation times |
HOST / PORT | 0.0.0.0 / 8110 | Web server |
GET / | Dish list |
GET /dishes/{id}, POST /dishes/{id} | Recipe editor and analysis |
POST /dishes/{id}/confirm | Confirm (needs a name and the recipe hash the page was opened with) |
GET /menu?lang=en|ru|tr|de | Printable menu |
GET /api/menu.json?lang= | Published dishes, legend, unpublished list |
GET /api/dishes | Every dish with its state and analysis |
POST /api/check | {"ingredients": "..."} → analysis, nothing saved |
uv run python -m app.evaluate runs the dictionary over
tests/fixtures/labelled_sample.csv: 116 ingredient lines as a
kitchen writes them (28 to 30 per language), which I labelled myself with the Annex II groups
they contain. It is my own sample, not an official data set.
| Result | |
|---|---|
| Expected allergens pre-selected for the chef | 104 of 104 |
| Silent misses (allergen not pre-selected and line not flagged) | 0 |
| Extra allergens pre-selected (the chef unticks them) | 5 (praline and krokant: egg, milk; mayonnaise: mustard) |
| Lines flagged for a person to check | 40 of 116 (first run: 42, see BUGFIX.md, bug 1) |
| Lines with no allergen expected and none pre-selected | 17 of 17 |
Most flags come from words the dictionary does not know ("tinned", "king", "высшего сорта", "köy", "Bio"). That is the intended trade-off: an unknown word is shown to a person rather than assumed harmless.
uv run pytest
uv run ruff check . && uv run ruff format --check .
220 tests: each of the 14 groups with a word in each of the four languages, Russian, Turkish and German morphology, false friends ("сырой" is not cheese, "Schweinefleisch" is not wine, "biraz" is not beer), every Annex II exclusion, free-from words, composite and bracketed products, ambiguous words, lines where nothing is recognised ("XO", garbled text), the confirmation rules (logging, reset on recipe change, no restore on revert, stale page, append-only log, drift), the Anthropic client against a fake HTTP server, the menu in every language, the JSON export, the CLI and the labelled sample.
app/
allergens.py the 14 Annex II groups: numbers, names in 4 languages, exclusions
dictionary.py curated EN/RU/TR/DE words, products, look-alikes, safe words
matcher.py morphology-aware patterns, longest-match selection, statuses
text.py folding (İ/ı, ß, ё, diacritics), recipe parsing, recipe hash
service.py dishes, confirmation rules, AI suggestion caching
db.py SQLite: dishes, append-only confirmations and events, AI cache
menu.py published menu and JSON export
web.py FastAPI routes; templates/ has the HTML
llm/ Anthropic client (structured output) and the offline stand-in
demo.py 16 sample dishes
cli.py `check` command and server start
evaluate.py accuracy on the labelled sample
tests/ pytest suite and the labelled sample
See BUGFIX.md.