Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions community/recipe-coach/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Recipe Coach

![Community](https://img.shields.io/badge/OpenHome-Community-orange?style=flat-square)
![Author](https://img.shields.io/badge/Author-@hueymahl-lightgrey?style=flat-square)

## What It Does

A voice-guided cooking assistant that generates recipes on the fly and walks you through them step by step. Name a dish or ingredient — Recipe Coach builds a recipe, reads the ingredients, then guides you through each step at your own pace.

No API keys required. Uses the Personality's built-in LLM for recipe generation.

## Suggested Trigger Words

- "recipe coach"
- "help me cook"
- "walk me through a recipe"
- "teach me to cook"
- "cooking guide"

## Setup

No setup required. Recipe Coach uses the Personality's built-in LLM — no external APIs or keys needed. Just upload, set your trigger words, and go.

## How It Works

1. **Ask** — Recipe Coach asks what you'd like to cook
2. **Generate** — The LLM creates a complete recipe with ingredients and steps
3. **Ingredients** — Reads the ingredient list in small, easy-to-follow groups
4. **Confirm** — Asks if you're ready to start cooking
5. **Step-by-step** — Walks through each step one at a time, waiting for your command

### Voice Commands During Cooking

| Say | What Happens |
|-----|-------------|
| **"next"** / "continue" / "go ahead" | Move to the next step |
| **"repeat"** / "again" / "say that again" | Hear the current step again |
| **"stop"** / "done" / "exit" | Leave the recipe at any time |

## Example Conversation

> **User:** "Help me cook"
>
> **AI:** "Hey! I'm your recipe coach. What would you like to cook today?"
>
> **User:** "Chicken stir fry"
>
> **AI:** "Great choice. Let me put together a recipe for chicken stir fry."
>
> **AI:** "Here's what you'll need for Chicken Stir Fry, serving 4."
>
> **AI:** "1 pound boneless chicken breast, sliced thin. 2 tablespoons soy sauce. 1 tablespoon sesame oil."
>
> **AI:** "1 red bell pepper, sliced. 2 cups broccoli florets. 3 cloves garlic, minced."
>
> **AI:** "Got everything? Say yes when you're ready to start cooking."
>
> **User:** "Yes"
>
> **AI:** "Let's go. 5 steps total. Say next to move on, repeat to hear a step again, or stop to exit."
>
> **AI:** "Step 1. Heat the sesame oil in a large skillet or wok over high heat."
>
> **User:** "Next"
>
> **AI:** "Step 2. Add the chicken and cook for 5 minutes until golden brown, then set aside."
>
> **User:** "Repeat"
>
> **AI:** "Step 2. Add the chicken and cook for 5 minutes until golden brown, then set aside."
>
> **User:** "Next"
>
> *(continues through remaining steps...)*
>
> **AI:** "That's it! Chicken Stir Fry is done. Enjoy your meal!"
Empty file.
173 changes: 173 additions & 0 deletions community/recipe-coach/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import json
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker

# =============================================================================
# RECIPE COACH
# A voice-guided cooking assistant. User names a dish or ingredient,
# the Ability generates a recipe and walks them through it step by step.
#
# Commands during cooking:
# "next" → advance to the next step
# "repeat" → hear the current step again
# "stop" → exit at any time
#
# No API keys required — uses the Personality's built-in LLM.
# =============================================================================

EXIT_WORDS = {"stop", "exit", "quit", "done", "cancel", "bye", "nevermind", "never mind"}
NEXT_WORDS = {"next", "continue", "go on", "next step", "keep going", "go ahead"}
REPEAT_WORDS = {"repeat", "again", "say that again", "what was that", "one more time"}


class RecipeCoachCapability(MatchingCapability):
worker: AgentWorker = None
capability_worker: CapabilityWorker = None

# Do not change following tag of register capability
# {{register capability}}

def call(self, worker: AgentWorker):
self.worker = worker
self.capability_worker = CapabilityWorker(self.worker)
self.worker.session_tasks.create(self.run())

def _user_wants_exit(self, text: str) -> bool:
lower = text.lower().strip()
return any(w in lower for w in EXIT_WORDS)

def _user_wants_next(self, text: str) -> bool:
lower = text.lower().strip()
return any(w in lower for w in NEXT_WORDS)

def _user_wants_repeat(self, text: str) -> bool:
lower = text.lower().strip()
return any(w in lower for w in REPEAT_WORDS)

def _generate_recipe(self, dish: str) -> dict:
"""Use the LLM to generate a structured recipe."""
prompt = (
f"Generate a recipe for: {dish}\n\n"
"Return ONLY valid JSON with this exact structure:\n"
"{\n"
' "title": "Recipe Name",\n'
' "servings": "number of servings",\n'
' "ingredients": ["ingredient 1", "ingredient 2"],\n'
' "steps": ["Step 1 instruction", "Step 2 instruction"]\n'
"}\n\n"
"Keep each step to one or two sentences. "
"Use simple, clear language suitable for reading aloud. "
"Include quantities in the ingredients list."
)
raw = self.capability_worker.text_to_text_response(prompt)
# Strip markdown fences if present
clean = raw.replace("```json", "").replace("```", "").strip()
try:
return json.loads(clean)
except (json.JSONDecodeError, TypeError):
return None

async def run(self):
try:
# --- Ask what they want to cook ---
await self.capability_worker.speak(
"Hey! I'm your recipe coach. What would you like to cook today?"
)
dish_input = await self.capability_worker.user_response()

if not dish_input or self._user_wants_exit(dish_input):
await self.capability_worker.speak("No worries. Come back when you're hungry!")
self.capability_worker.resume_normal_flow()
return

# --- Generate the recipe ---
await self.capability_worker.speak(
f"Great choice. Let me put together a recipe for {dish_input}."
)

recipe = self._generate_recipe(dish_input)

if not recipe or "steps" not in recipe or "ingredients" not in recipe:
self.worker.editor_logging_handler.error(
f"[RecipeCoach] Failed to parse recipe for: {dish_input}"
)
await self.capability_worker.speak(
"Sorry, I couldn't put that recipe together. Try asking for a different dish."
)
self.capability_worker.resume_normal_flow()
return

title = recipe.get("title", dish_input)
servings = recipe.get("servings", "a few")
ingredients = recipe["ingredients"]
steps = recipe["steps"]

# --- Read ingredients ---
await self.capability_worker.speak(
f"Here's what you'll need for {title}, serving {servings}."
)
# Group ingredients into chunks of 3-4 for natural voice delivery
for i in range(0, len(ingredients), 3):
chunk = ingredients[i:i + 3]
ingredient_text = ". ".join(chunk) + "."
await self.capability_worker.speak(ingredient_text)

# --- Confirm ready to cook ---
ready = await self.capability_worker.run_confirmation_loop(
"Got everything? Say yes when you're ready to start cooking."
)

if not ready:
await self.capability_worker.speak(
"No problem. The recipe will be here when you're ready. Goodbye!"
)
self.capability_worker.resume_normal_flow()
return

# --- Walk through steps ---
await self.capability_worker.speak(
f"Let's go. {len(steps)} steps total. Say next to move on, "
"repeat to hear a step again, or stop to exit."
)

step_index = 0
while step_index < len(steps):
step_num = step_index + 1
step_text = f"Step {step_num}. {steps[step_index]}"
await self.capability_worker.speak(step_text)

# Wait for user command (unless it's the last step)
if step_index < len(steps) - 1:
user_input = await self.capability_worker.user_response()

if not user_input:
continue

if self._user_wants_exit(user_input):
await self.capability_worker.speak(
f"Stopping at step {step_num}. Good luck with the rest!"
)
self.capability_worker.resume_normal_flow()
return

if self._user_wants_repeat(user_input):
continue # Don't increment — repeat same step

# "next" or anything else advances
step_index += 1
else:
step_index += 1

# --- Done ---
await self.capability_worker.speak(
f"That's it! {title} is done. Enjoy your meal!"
)

except Exception as e:
self.worker.editor_logging_handler.error(f"[RecipeCoach] Error: {e}")
await self.capability_worker.speak(
"Something went wrong. Let's try again later."
)

self.capability_worker.resume_normal_flow()