Files
Epicure/apps/web/lib/ai/tools/create-recipe-tool.ts
Arnaud 982d4e3264 feat: cooking assistant can propose creating a recipe
Gives the general chat a createRecipe tool (Vercel AI SDK, stepCountIs(3))
scoped so it can only ever produce a draft — the tool's execute is a pure
echo, no DB write. The route surfaces the tool call as proposedRecipe
alongside the normal text answer; the chat UI renders it as a card with
explicit Create/Discard buttons. Create POSTs to the existing
/api/v1/recipes endpoint — the same code path and tier/recipe-count limit
the manual editor already goes through — so there's exactly one place
that actually creates a recipe row, and nothing happens without the user
clicking Create.

Scoped to the general assistant only (not per-recipe chat), and to one
tool for now — addToShoppingList/generateMealPlan are follow-ups.

v0.45.0
2026-07-17 18:26:19 +02:00

39 lines
1.9 KiB
TypeScript

import { tool } from "ai";
import { z } from "zod";
const CreateRecipeInputSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
baseServings: z.number().int().min(1).max(100).default(4),
recipeType: z.enum(["dish", "drink"]).default("dish"),
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
prepMins: z.number().int().min(0).max(1440).optional(),
cookMins: z.number().int().min(0).max(1440).optional(),
ingredients: z.array(z.object({
rawName: z.string().min(1).max(200).describe("Ingredient name only, e.g. 'flour' — never combined with quantity/unit"),
quantity: z.number().optional().describe("A number only, e.g. 0.25, 1.5, 2"),
unit: z.string().max(50).optional().describe("e.g. 'cup', 'tbsp', 'g'"),
})).min(1).max(100),
steps: z.array(z.object({
instruction: z.string().min(1).max(2000),
})).min(1).max(100),
});
export type CreateRecipeToolInput = z.infer<typeof CreateRecipeInputSchema>;
/**
* Lets the cooking assistant propose a new recipe from the conversation.
* Deliberately does NOT write to the database — `execute` just validates and
* echoes the input back as a draft. The chat UI renders that draft with an
* explicit Confirm button, which POSTs to the existing /api/v1/recipes
* endpoint (the same one the manual recipe editor uses) — so nothing gets
* created without the user directly approving it, and there's exactly one
* code path that actually writes a recipe row.
*/
export const createRecipeTool = tool({
description:
"Propose creating a new recipe based on the conversation. This only drafts the recipe for the user to review — it does not save anything. Only call this when the user has actually asked you to create/save/write down a recipe, not just discussed one.",
inputSchema: CreateRecipeInputSchema,
execute: async (input: CreateRecipeToolInput) => input,
});