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:
@@ -0,0 +1,27 @@
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
const AddToShoppingListInputSchema = z.object({
|
||||
items: z.array(z.object({
|
||||
rawName: z.string().min(1).max(200).describe("Ingredient/item 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),
|
||||
suggestedListName: z.string().max(100).optional().describe("Suggested name if the user wants a new list, e.g. 'Weekend BBQ'"),
|
||||
});
|
||||
|
||||
export type AddToShoppingListToolInput = z.infer<typeof AddToShoppingListInputSchema>;
|
||||
|
||||
/**
|
||||
* Lets the cooking assistant propose adding items to a shopping list — same
|
||||
* safety shape as createRecipeTool: `execute` only validates/echoes the
|
||||
* input, no DB write. The chat UI resolves which list (existing or new)
|
||||
* and POSTs through the existing /api/v1/shopping-lists(/{id}/items)
|
||||
* endpoints, the same ones AddToShoppingListButton already uses.
|
||||
*/
|
||||
export const addToShoppingListTool = tool({
|
||||
description:
|
||||
"Propose adding ingredients/items to a shopping list, e.g. from a recipe just discussed. This only drafts the item list for the user to review and pick a target list — it does not save anything. Only call this when the user has actually asked to add items to a shopping list.",
|
||||
inputSchema: AddToShoppingListInputSchema,
|
||||
execute: async (input: AddToShoppingListToolInput) => input,
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
||||
export const APP_VERSION = "0.45.0";
|
||||
export const APP_VERSION = "0.46.0";
|
||||
|
||||
export type ChangelogEntry = {
|
||||
version: string;
|
||||
@@ -11,6 +11,14 @@ export type ChangelogEntry = {
|
||||
};
|
||||
|
||||
export const CHANGELOG: ChangelogEntry[] = [
|
||||
{
|
||||
version: "0.46.0",
|
||||
date: "2026-07-17 18:00",
|
||||
added: [
|
||||
"The cooking assistant can now also propose adding items to a shopping list (\"add that to my shopping list\") — pick an existing list or name a new one, then confirm. Same no-action-without-confirmation pattern as recipe creation, going through the same endpoints the manual \"Add to shopping list\" button already uses.",
|
||||
],
|
||||
notes: "generateMealPlan is still not built as a chat tool — the existing meal-plan-generation endpoint generates and saves in one step with its own input shape (week/preferences, not a specific plan to save), so it doesn't fit the draft-then-confirm pattern the other two tools use without a larger change to that endpoint.",
|
||||
},
|
||||
{
|
||||
version: "0.45.0",
|
||||
date: "2026-07-17 17:30",
|
||||
|
||||
@@ -320,7 +320,11 @@ export function generateOpenApiSpec(): object {
|
||||
ingredients: z.array(z.object({ rawName: z.string(), quantity: z.number().optional(), unit: z.string().optional() })),
|
||||
steps: z.array(z.object({ instruction: z.string() })),
|
||||
}));
|
||||
registry.registerPath({ method: "post", path: "/api/v1/ai/cooking-chat", summary: "Ask the general (not recipe-specific) cooking assistant a question", description: "Rate-limited: 30 req/min. Consumes AI quota. Returns a single JSON response (not streamed); persists the exchange to chat history. Pass conversationId (from POST /api/v1/ai/conversations) to group messages into a named conversation and auto-title it from the first question. If the user asks the assistant to create/save a recipe, the response may include proposedRecipe — a draft the assistant proposed but did NOT save; the caller must POST it to /api/v1/recipes itself to actually create it.", security, request: { body: { content: { "application/json": { schema: z.object({ question: z.string().min(1).max(500), conversationId: z.string().uuid().optional() }) } }, required: true } }, responses: { 200: { description: "Answer", content: { "application/json": { schema: z.object({ answer: z.string(), proposedRecipe: ProposedRecipeRef.optional() }) } } }, 400: { description: "Validation error", content: { "application/json": { schema: ApiErrorRef } } }, 429: { description: "Rate limited or AI quota exhausted", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
const ProposedShoppingListRef = registry.register("ProposedShoppingList", z.object({
|
||||
items: z.array(z.object({ rawName: z.string(), quantity: z.number().optional(), unit: z.string().optional() })),
|
||||
suggestedListName: z.string().optional(),
|
||||
}));
|
||||
registry.registerPath({ method: "post", path: "/api/v1/ai/cooking-chat", summary: "Ask the general (not recipe-specific) cooking assistant a question", description: "Rate-limited: 30 req/min. Consumes AI quota. Returns a single JSON response (not streamed); persists the exchange to chat history. Pass conversationId (from POST /api/v1/ai/conversations) to group messages into a named conversation and auto-title it from the first question. If the user asks the assistant to create/save a recipe or add items to a shopping list, the response may include proposedRecipe / proposedShoppingList — drafts the assistant proposed but did NOT save; the caller must POST them itself (to /api/v1/recipes, or /api/v1/shopping-lists + /api/v1/shopping-lists/{id}/items) to actually create anything.", security, request: { body: { content: { "application/json": { schema: z.object({ question: z.string().min(1).max(500), conversationId: z.string().uuid().optional() }) } }, required: true } }, responses: { 200: { description: "Answer", content: { "application/json": { schema: z.object({ answer: z.string(), proposedRecipe: ProposedRecipeRef.optional(), proposedShoppingList: ProposedShoppingListRef.optional() }) } } }, 400: { description: "Validation error", content: { "application/json": { schema: ApiErrorRef } } }, 429: { description: "Rate limited or AI quota exhausted", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
const AiConversationRef = registry.register("AiConversation", z.object({
|
||||
id: z.string(), title: z.string().nullable(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user