Files
MealQuest/internal/server/ideas.go
T
dev_agentandClaude Opus 5.5 290e3d3cd3
CI / ci (push) Successful in 5m36s
CI / ci (pull_request) Successful in 5m34s
Add quick_ideas skill and job for Quick dinners (Feature 18)
quick_ideas v1 suggests Quick dinners of one kind, crockpot or under30,
from the household context, never returning a name already shown: the
exclude list is checked against normalized names with harness.Similar.
POST /api/v1/ideas/quick {kind, exclude[]} validates both fields (no
default kind) and answers 202 {jobId}; the quick_ideas job asks for 12
ideas, grounds them through the same GroundEach rounds as
ideas_from_ingredients and keeps up to 6 whose grounded recipe is of the
kind (crockpot: method crockpot; under30: minutes_total <= 30), so the
model's estimate never decides. Fewer survivors make a shorter page,
never a padded one.

The grounding loop, failure clearing and failed-result view are now
shared generic helpers used by both idea jobs rather than a second copy.

Maestro task 243a6812-f52b-4215-a4a2-8eb599914f7e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:25:41 -04:00

96 lines
3.4 KiB
Go

package server
import (
"errors"
"log/slog"
"net/http"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ideas"
)
// maxIdeasBody bounds a POST /ideas/from-ingredients or /ideas/quick body.
const maxIdeasBody = 16 << 10
// ideasFromIngredients is the POST /ideas/from-ingredients body. Both
// fields are required.
type ideasFromIngredients struct {
Ingredients []string `json:"ingredients"`
QuickOnly *bool `json:"quick_only"`
}
// postIdeasFromIngredients starts finding ideas from the ingredients on
// hand and answers 202 with the jobId that GET /jobs/{id} reports on,
// without waiting for the job. An ingredient list the household must
// correct is 400.
func postIdeasFromIngredients(logger *slog.Logger, svc *ideas.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := decodePatch[ideasFromIngredients](http.MaxBytesReader(w, r.Body, maxIdeasBody))
if err != nil {
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
return
}
switch {
case body.Ingredients == nil:
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", "ingredients: required")
return
case body.QuickOnly == nil:
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", "quick_only: required")
return
}
started, err := svc.StartFromIngredients(r.Context(), body.Ingredients, *body.QuickOnly)
var invalid *ideas.InvalidError
switch {
case errors.As(err, &invalid):
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", invalid.Message)
return
case err != nil:
logger.ErrorContext(r.Context(), "start ideas from ingredients", "error", err)
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
return
}
writeJSON(w, r, logger, http.StatusAccepted, started)
}
}
// ideasQuick is the POST /ideas/quick body. Both fields are required:
// exclude is [] on a first page.
type ideasQuick struct {
Kind *string `json:"kind"`
Exclude []string `json:"exclude"`
}
// postIdeasQuick starts finding a page of Quick dinner ideas of a kind,
// crockpot or under30, none named in exclude, and answers 202 with the
// jobId that GET /jobs/{id} reports on, without waiting for the job. A
// missing or unknown kind, or an exclude list the household must correct,
// is 400.
func postIdeasQuick(logger *slog.Logger, svc *ideas.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := decodePatch[ideasQuick](http.MaxBytesReader(w, r.Body, maxIdeasBody))
if err != nil {
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
return
}
switch {
case body.Kind == nil:
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", "kind: required")
return
case body.Exclude == nil:
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", "exclude: required")
return
}
started, err := svc.StartQuick(r.Context(), *body.Kind, body.Exclude)
var invalid *ideas.InvalidError
switch {
case errors.As(err, &invalid):
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", invalid.Message)
return
case err != nil:
logger.ErrorContext(r.Context(), "start quick ideas", "error", err)
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
return
}
writeJSON(w, r, logger, http.StatusAccepted, started)
}
}