Grounding pipeline and recipe-search settings API
recipes.Grounder ties search, fetch/extract, Rank, the photo cache and
normalize_recipe together. Ground(ctx, dishName, query) reuses a stored
source_url among the top candidates without fetching, otherwise tries the
ranked recipes best first and stores the photo, recipe, ingredients and
steps in one transaction, returning Grounded{recipe_id, photo_id, method,
minutes_total, difficulty, source_url, site_name} or a typed
*ErrUngroundable listing every reason. GroundURL grounds a pasted URL the
same way; GroundMany grounds items 3 at a time with per-item results.
PhotoCache is split into DownloadPhoto (fetch, decode, size check, no
writes) and CachePhoto(ctx, tx, photo), which records the row in the
caller's transaction; RemovePhotoFiles cleans up after a rollback.
Migration 0007 adds photo_id, method, difficulty and minutes_total to
recipes, and api_key_last4 to recipe_search_settings. SearXNG is now
called at recipe_search_settings.base_url.
GET/PATCH /api/v1/settings/recipe-search (Brave key sealed with
secretbox, only api_key_set and api_key_last4 returned) and POST
/api/v1/settings/recipe-search/test (searches "chicken tikka masala",
returns the count, 409 not_configured or 502 search_failed).
Maestro task 53afd59c-6c35-4af7-904c-6c167d1ad69d.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/recipes"
|
||||
)
|
||||
|
||||
// searchTestResponse is the POST /settings/recipe-search/test body.
|
||||
type searchTestResponse struct {
|
||||
Query string `json:"query"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func getRecipeSearchSettings(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := svc.Get(r.Context())
|
||||
if err != nil {
|
||||
logger.ErrorContext(r.Context(), "get recipe search settings", "error", err)
|
||||
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, r, logger, http.StatusOK, settings)
|
||||
}
|
||||
}
|
||||
|
||||
// patchRecipeSearchSettings takes the Brave API key in api_key; the
|
||||
// response, like GET's, never holds it.
|
||||
func patchRecipeSearchSettings(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
patch, err := decodePatch[recipes.SearchSettingsPatch](http.MaxBytesReader(w, r.Body, maxSettingsBody))
|
||||
if err != nil {
|
||||
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
|
||||
return
|
||||
}
|
||||
settings, err := svc.Update(r.Context(), patch)
|
||||
var invalid *recipes.SearchSettingsError
|
||||
if errors.As(err, &invalid) {
|
||||
writeError(w, r, logger, http.StatusBadRequest, "invalid_settings", invalid.Error())
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.ErrorContext(r.Context(), "update recipe search settings", "error", err)
|
||||
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, r, logger, http.StatusOK, settings)
|
||||
}
|
||||
}
|
||||
|
||||
// postRecipeSearchTest searches for recipes.SearchTestQuery with the saved
|
||||
// settings. Recipe search not configured is 409; a search that fails is
|
||||
// 502 with the provider's error, which never holds the API key.
|
||||
func postRecipeSearchTest(logger *slog.Logger, svc *recipes.SearchSettingsService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
count, err := svc.TestSearch(r.Context())
|
||||
if errors.Is(err, recipes.ErrSearchNotConfigured) {
|
||||
writeError(w, r, logger, http.StatusConflict, "not_configured", "recipe search is not configured")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger.WarnContext(r.Context(), "recipe search test failed", "error", err)
|
||||
writeError(w, r, logger, http.StatusBadGateway, "search_failed", err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, r, logger, http.StatusOK, searchTestResponse{Query: recipes.SearchTestQuery, Count: count})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user