Implement the ideas_from_ingredients v1 skill on the AI harness: it asks
for 8 ideas {name, uses[], missing[], est_minutes, method, difficulty,
search_query}, keeps only "uses" entries whose harness.Normalize form is
one of the household's listed ingredients (written as the household wrote
them, in its order), drops ideas using none or repeating another, and
computes the summary line from the corrected lists.
Add internal/ideas: an ideas_from_ingredients job that grounds the ideas
with Grounder.GroundMany in the model's order until 5 are kept, drops the
ungroundable ones and, for quick_only, those whose grounded recipe is not
a Quick dinner (crockpot or minutes_total <= 30), publishing the result
as each idea is kept. POST /api/v1/ideas/from-ingredients answers 202
{jobId}.
Maestro task c3c5f98e-7f7d-4c6b-a1ab-d45bb8e497f9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
1.8 KiB
Go
54 lines
1.8 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 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)
|
|
}
|
|
}
|