feat: cooking assistant can propose adding items to a shopping list
Second tool alongside createRecipe, same safety shape: addToShoppingList's execute only validates/echoes input, no DB write. The proposal card lets the user pick an existing list (fetched lazily) or name a new one, then confirms through the exact two-call flow AddToShoppingListButton already uses — POST /api/v1/shopping-lists (if new) then POST .../items — no new persistence code path. generateMealPlan intentionally not built as a tool: the existing /api/v1/ai/meal-plan/generate endpoint generates and writes in one step with a week/preferences input, not a specific plan payload, so it has no "save this exact draft" entry point to confirm against without a larger refactor. Documented as a known gap rather than forcing a weaker pattern. v0.46.0
This commit is contained in:
@@ -9,6 +9,7 @@ import { getModelConfigForUseCase } from "@/lib/ai/resolve-user-key";
|
||||
import { resolveModel } from "@/lib/ai/factory";
|
||||
import { getUserPrivateBio, buildUserBioContext } from "@/lib/ai/user-bio";
|
||||
import { createRecipeTool } from "@/lib/ai/tools/create-recipe-tool";
|
||||
import { addToShoppingListTool } from "@/lib/ai/tools/add-to-shopping-list-tool";
|
||||
|
||||
const Schema = z.object({
|
||||
question: z.string().min(1).max(500),
|
||||
@@ -44,9 +45,9 @@ export async function POST(req: NextRequest) {
|
||||
const result = await withAiQuota(session!.user.id, session!.user.tier as "free" | "pro" | "team", () =>
|
||||
generateText({
|
||||
model,
|
||||
system: `You are Epicure, a helpful culinary assistant answering general cooking questions — not tied to any specific recipe (techniques, substitutions, timing, equipment, food safety, etc). If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question has nothing to do with cooking or food, politely redirect. Keep answers under 200 words. Respond in ${lang}.\n\nYou can propose creating a recipe with the createRecipe tool when the user explicitly asks you to create, save, or write down a recipe — e.g. "make me a recipe for X" or "write that down as a recipe". Don't call it just because a recipe came up in conversation. When you do call it, still write a short text reply too (e.g. "Here's a draft — check it below and confirm if it looks right"), since calling the tool only drafts the recipe for the user to review; it never saves anything by itself.${bioContext}`,
|
||||
system: `You are Epicure, a helpful culinary assistant answering general cooking questions — not tied to any specific recipe (techniques, substitutions, timing, equipment, food safety, etc). If asked who you are or what model/AI you're built on, say you're Epicure — never name the underlying model or provider. If a question has nothing to do with cooking or food, politely redirect. Keep answers under 200 words. Respond in ${lang}.\n\nYou have two tools, both of which only draft something for the user to review — neither saves anything by itself, so still write a short text reply too when you use one (e.g. "Here's a draft — check it below and confirm if it looks right").\n- createRecipe: only when the user explicitly asks you to create, save, or write down a recipe (e.g. "make me a recipe for X", "write that down as a recipe"). Don't call it just because a recipe came up in conversation.\n- addToShoppingList: only when the user explicitly asks to add ingredients/items to a shopping list (e.g. "add that to my shopping list").${bioContext}`,
|
||||
prompt: parsed.data.question,
|
||||
tools: { createRecipe: createRecipeTool },
|
||||
tools: { createRecipe: createRecipeTool, addToShoppingList: addToShoppingListTool },
|
||||
stopWhen: stepCountIs(3),
|
||||
}), { skipQuota: aiConfig.isByok }
|
||||
);
|
||||
@@ -54,6 +55,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const { conversationId } = parsed.data;
|
||||
const proposedRecipe = result.data.toolCalls.find((c) => c.toolName === "createRecipe")?.input;
|
||||
const proposedShoppingList = result.data.toolCalls.find((c) => c.toolName === "addToShoppingList")?.input;
|
||||
|
||||
void db.insert(chatMessages).values([
|
||||
{ id: crypto.randomUUID(), userId: session!.user.id, recipeId: null, conversationId, role: "user", content: parsed.data.question },
|
||||
@@ -72,5 +74,5 @@ export async function POST(req: NextRequest) {
|
||||
`).catch((err) => console.error("[cooking-chat] failed to touch conversation", err));
|
||||
}
|
||||
|
||||
return NextResponse.json({ answer: result.data.text, proposedRecipe });
|
||||
return NextResponse.json({ answer: result.data.text, proposedRecipe, proposedShoppingList });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user