diff --git a/community/recipe-coach/README.md b/community/recipe-coach/README.md new file mode 100644 index 0000000..3e65ef0 --- /dev/null +++ b/community/recipe-coach/README.md @@ -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!" diff --git a/community/recipe-coach/__init__.py b/community/recipe-coach/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community/recipe-coach/main.py b/community/recipe-coach/main.py new file mode 100644 index 0000000..93c68a9 --- /dev/null +++ b/community/recipe-coach/main.py @@ -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()