π A tiny web app that generates a season-aware ramen recipe (style, broth, tare, noodles, toppings, garnish, steps, shopping list).
π§ Built with Ruby (Sinatra + Puma), JSON-schema validation, and the OpenAI Agents API.
πΎ Repo: https://github.com/swallace100/ramen-chef-agent
Intro
Many AI recipe demos just post unstructured text. I wanted something that was testable and that follows a set format. This "ramen chefβ agent app knows that itβs October in Tokyo, suggests seasonal ingredients, and returns a clean JSON plan that the UI can render without guesswork.
This app serves a single page (vanilla JS) and one API endpoint. It builds a small season context, asks the model to respond as strict JSON, validates that JSON against a schema, and renders a tidy plan in Japanese or English
Tech Stack
- Language: Ruby 3.4+
- Framework: Sinatra + Puma
- Libraries: ruby-openai, oj, json_schemer, dotenv, rack-cors
- Tools: Rake tasks (setup, dev, run, lint, test)
- Data: data/japanese_seasonal.yml (month β suggested ingredients)
Everything runs locally.
Architecture Overview
.
ββ app.rb # Sinatra app: HTML UI + API routes
ββ services/
β ββ ramen_agent.rb # Agent: schema, prompt, OpenAI call,
ββ public/
β ββ app.js # Fetch + render (safe, resilient)
ββ data/
β ββ japanese_seasonal.yml # Month β seasonal ingredients
ββ puma.rb # Puma config
ββ Rakefile # setup/dev/run tasks
ββ resources/
β ββ.env.sample # copy to .env with OPENAI_API_KEY
ββ config.ru # rack entry
Flow:
Browser β Sinatra (/api/recommend) β RamenAgent β OpenAI (JSON) β JSON Schema validation β Rendered plan
All requests are validated. If the model drifts, the app returns a graceful fallback instead of breaking the UI.
JSON-First Planning
The agent enforces a schema so the UI never has to guess field names.
# services/ramen_agent.rb (excerpt)
Schema = {
"type" => "object",
"properties" => {
"season_context" => {
"type" => "object",
"properties" => {
"date" => { "type" => "string" },
"month" => { "type" => "string" },
"location" => { "type" => "string" },
"suggested_ingredients" => { "type" => "array", "items" => { "type" => "string" } }
},
"required" => %w[date month location suggested_ingredients],
"additionalProperties" => false
},
"style" => { "type" => "string" },
"broth" => { "type" => "string" },
"tare" => { "type" => "string" },
"noodles" => { "type" => "string" },
"toppings" => { "type" => "array", "items" => { "type" => "string" } },
"garnish" => { "type" => "array", "items" => { "type" => "string" } },
"method_steps" => { "type" => "array", "items" => { "type" => "string" } },
"shopping_list" => { "type" => "array", "items" => { "type" => "string" } },
"serving_note" => { "type" => "string" }
},
"required" => %w[
season_context style broth tare noodles toppings garnish method_steps shopping_list serving_note
],
"additionalProperties" => false
}
Validation code:
schemer = JSONSchemer.schema(Schema)
plan = Oj.load(model_json, mode: :strict)
raise "invalid" unless schemer.valid?(plan)
Frontend
A minimal page with a Generate Plan button. The JS shows a loading state, escapes strings (XSS-safe), renders lists, and displays a βRaw JSONβ toggle for debugging.
// public/app.js (excerpt)
const $ = (id) => document.getElementById(id);
document.addEventListener("DOMContentLoaded", () => {
$("go")?.addEventListener("click", async () => {
const box = $("result");
box.style.display = "block";
box.textContent = "Generatingβ¦";
const payload = {
location: $("location")?.value || "",
language: $("language")?.value || "ja-JP",
notes: $("notes")?.value || ""
};
const resp = await fetch("/api/recommend", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await resp.json();
// renderPlan(box, data) β outputs Style/Broth/Tare/Noodles/etc.
});
});
Environment Setup
- Clone + env
git clone https://github.com/yourname/seasonal-ramen-chef
cd seasonal-ramen-chef
cp .env.sample .env # add your OPENAI_API_KEY=...
- Install (project-local gems)
bundle exec rake setup
- Run (auto-reload)
bundle exec rake dev
# open http://localhost:4567
(Prefer plain run? bundle exec rake run)
Example Request
curl -s -X POST http://localhost:4567/api/recommend \
-H "Content-Type: application/json" \
-d '{"location":"Osaka","language":"en-US","notes":"no onions please"}'
Returns a JSON plan with style, broth, tare, noodles, toppings, garnish, method_steps, shopping_list, and serving_note, plus a season_context like { date, month, location, suggested_ingredients }.
Lessons Learned
- Including a schema makes it so users get output with the same style every time and a missed field won't break the UI.
- Month + locale gives the agent a good baseline for ingredient suggestions without the need for more prompting.
- ja-JP vs en-US makes it useful in Japan and abroad.
Repository + License
π Full source: https://github.com/swallace100/ramen-chef-agent
βοΈ License: MIT
Possible future features
- A mode to get a recipe based on ingredients the user has on hand.
- Diet/allergen filters (vegetarian, pork-free, nut-free).
- Weekly planner export (PDF/Markdown).
- Budget/servings sliders and rough price hints per bowl.
- More food types beyond ramen.
If you fork this and add a new feature, drop a link and share!

Top comments (0)