···11+overlay: 1.0.0
22+info:
33+ title: Kroger Products client generation overlay
44+ version: 0.0.0
55+actions:
66+ - target: $.components.schemas["products.productModel"].properties.nutritionInformation
77+ description: Drop field because Kroger returns both object and array shapes and staples does not use it.
88+ remove: true
99+ - target: $.components.schemas["products.productModel"].properties.sweeteningMethods
1010+ description: Drop field because Kroger returns both object and array shapes and staples does not use it.
1111+ remove: true
1212+ - target: $.components.schemas["products.productItemPriceModel"].properties.expirationDate
1313+ description: Drop field because Kroger returns date-time strings while the spec declares date-only values and staples does not use it.
1414+ remove: true
1515+ - target: $.components.schemas["products.productItemPriceModel"].properties.effectiveDate
1616+ description: Drop field because Kroger returns date-time strings while the spec declares date-only values and staples does not use it.
1717+ remove: true
···16161717 "github.com/samber/lo"
1818 "github.com/samber/lo/mutable"
1919+ "go.opentelemetry.io/otel"
2020+ "go.opentelemetry.io/otel/attribute"
1921)
20222123type aiClient interface {
···3941 statusWriter statusWriter
4042}
41434444+var tracer = otel.Tracer("careme/internal/recipes")
4545+4246func NewGenerator(aiClient aiClient, critiquer critique.Service, staples staplesService, statuses statusWriter) (*generatorService, error) {
4347 if aiClient == nil {
4448 return nil, fmt.Errorf("ai client is required")
···5862}
59636064func (g *generatorService) PickAWine(ctx context.Context, location string, recipe ai.Recipe, date time.Time) (*ai.WineSelection, error) {
6565+ ctx, span := tracer.Start(ctx, "recipes.pickawine")
6666+ defer span.End()
6167 var styles []string
6268 for _, style := range recipe.WineStyles {
6369 style = strings.TrimSpace(style)
···102108 // if we have a response id one of the three should be true? Or did they just not care and hit try again?
103109 if p.ResponseID != "" && (p.Instructions != "" || len(p.Saved) > 0 || len(p.Dismissed) > 0) {
104110 slog.InfoContext(ctx, "Regenerating recipes for location", "location", p.String(), "response_id", p.ResponseID)
111111+ ctx, span := tracer.Start(ctx, "recipes.regenerate")
112112+ defer span.End()
105113 instructions := regenerateInstructions(p)
106114115115+ // TODO give them some sort of status.
107116 shoppingList, err := g.aiClient.Regenerate(ctx, instructions, p.ResponseID)
108117 if err != nil {
109118 return nil, fmt.Errorf("failed to regenerate recipes with AI: %w", err)
···124133 return shoppingList, nil
125134 }
126135136136+ ctx, span := tracer.Start(ctx, "recipes.generate")
137137+ defer span.End()
127138 slog.InfoContext(ctx, "Generating recipes for location", "location", p.String())
128139 ingredients, err := g.staples.FetchStaples(ctx, p)
129140 if err != nil {
130141 return nil, fmt.Errorf("failed to get staples: %w", err)
131142 }
132132- g.writeStatus(ctx, hash, fmt.Sprintf("Looking through %d ingredients", len(ingredients)))
143143+ ogCount := len(ingredients)
133144 ingredients = lo.Filter(ingredients, func(ing ai.InputIngredient, _ int) bool {
134145 // TODO make configurable?
135135- return ing.Grade == nil || ing.Grade.Score > 5
146146+ return ing.Grade == nil || ing.Grade.Score > 6
136147 })
148148+ // having category would be interesing here.
149149+ g.writeStatus(ctx, hash, fmt.Sprintf("Considering %d out of %d ingredients", len(ingredients), ogCount))
150150+137151 mutable.Shuffle(ingredients)
138152139153 instructions := []string{p.Directive, p.Instructions}
154154+140155 shoppingList, err := g.aiClient.GenerateRecipes(ctx, p.Location, ingredients, instructions, p.Date, p.LastRecipes)
141156 if err != nil {
142157 return nil, fmt.Errorf("failed to generate recipes with AI: %w", err)
···206221 if g.critiquer == nil {
207222 return shoppingList, nil
208223 }
224224+ ctx, span := tracer.Start(ctx, "recipes.critique")
225225+ defer span.End()
226226+209227 g.writeStatus(ctx, hash, titles("Getting feeeback on these recipes:", shoppingList.Recipes))
210228 results := g.critiquer.CritiqueRecipes(ctx, shoppingList.Recipes)
211229 good, garbage := critique.Split(ctx, results, critique.MinimumRecipeScore)
···215233 if len(garbage) == 0 {
216234 return shoppingList, nil
217235 }
236236+ span.SetAttributes(attribute.Bool("regenaftercrique", true))
218237 slog.InfoContext(ctx, "Regenerating recipes based on critique feedback:", "garbage_count", len(garbage), "good_count", len(good))
219238 garbageRecipes := lo.Map(garbage, func(r critique.Result, _ int) ai.Recipe { return *r.Recipe })
220239 g.writeStatus(ctx, hash, titles("Making adjustments to these recipes: ", garbageRecipes))
+2-2
internal/recipes/generator_hash_test.go
···2323 }
24242525 // make sure we're intentional about breaking hash
2626- if h1 != "JjKXkKjKKpE" {
2626+ if h1 != "wrxx3dmHzBA" {
2727 t.Fatalf("expected hash to be stable and equal to JjKXkKjKKpE, got %s", h1)
2828 }
2929···3131 if !ok {
3232 t.Fatal("expected current hash passhed to legacy")
3333 }
3434- if legacyHash != "cmVjaXBlJjKXkKjKKpE=" {
3434+ if legacyHash != "cmVjaXBlwrxx3dmHzBA=" {
3535 t.Fatalf("expected legacy hash to be base64 of recipe hash with prefix, got %s", legacyHash)
3636 }
3737
···108108 }
109109 }()
110110111111- // this should be back compat with kroger.Ingredient
112111 var ingredients []ai.InputIngredient
113112 if err := json.NewDecoder(ingredientBlob).Decode(&ingredients); err != nil {
114113 return nil, err