ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Tsvtollm (#304)

* csv

* okay lets go with tsv

* give number of ingredients in tsv

* few fixes from review

* quite mucking around

* utf8 baby

* missed an error

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
4a203f13 352e7f37

+152 -47
+4 -4
go.mod
··· 9 9 require ( 10 10 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 11 11 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 12 - github.com/alpkeskin/gotoon v0.1.1 13 12 github.com/clerk/clerk-sdk-go/v2 v2.5.0 14 13 github.com/invopop/jsonschema v0.13.0 14 + github.com/microsoft/ApplicationInsights-Go v0.4.4 15 15 github.com/openai/openai-go/v3 v3.21.0 16 16 github.com/openclosed-dev/slogan v0.2.0 17 17 github.com/samber/slog-multi v1.5.0 18 18 github.com/sendgrid/rest v2.6.9+incompatible 19 19 github.com/sendgrid/sendgrid-go v3.16.1+incompatible 20 + golang.org/x/crypto v0.40.0 20 21 golang.org/x/net v0.42.0 21 22 ) 22 23 ··· 27 28 github.com/buger/jsonparser v1.1.1 // indirect 28 29 github.com/go-jose/go-jose/v3 v3.0.4 // indirect 29 30 github.com/gofrs/uuid v3.3.0+incompatible // indirect 30 - github.com/microsoft/ApplicationInsights-Go v0.4.4 // indirect 31 31 github.com/samber/slog-common v0.19.0 // indirect 32 32 github.com/stretchr/testify v1.11.1 // indirect 33 33 github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 34 - golang.org/x/crypto v0.40.0 // indirect 35 34 golang.org/x/sync v0.16.0 // indirect 35 + golang.org/x/sys v0.34.0 // indirect 36 36 ) 37 37 38 38 require ( ··· 58 58 github.com/tidwall/sjson v1.2.5 // indirect 59 59 github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect 60 60 golang.org/x/mod v0.25.0 // indirect 61 - golang.org/x/text v0.27.0 // indirect 61 + golang.org/x/text v0.27.0 62 62 golang.org/x/tools v0.34.0 // indirect 63 63 gopkg.in/yaml.v2 v2.4.0 // indirect 64 64 gopkg.in/yaml.v3 v3.0.1 // indirect
+2 -2
go.sum
··· 13 13 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 14 14 github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 15 15 github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 16 - github.com/alpkeskin/gotoon v0.1.1 h1:GQOVwMfWKINnfEA6slrXHJaJYDwnUFmrPlXOtnuja1w= 17 - github.com/alpkeskin/gotoon v0.1.1/go.mod h1:XRTz8RM4tz8M2nB37MNRN8rHF4YgeYd8nIXmoU0B0+M= 18 16 github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= 19 17 github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= 20 18 github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= ··· 230 228 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 231 229 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 232 230 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 231 + golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 232 + golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 233 233 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 234 234 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 235 235 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+6 -11
internal/ai/client.go
··· 13 13 "strings" 14 14 "time" 15 15 16 - "github.com/alpkeskin/gotoon" 17 16 openai "github.com/openai/openai-go/v3" 18 17 "github.com/openai/openai-go/v3/conversations" 19 18 "github.com/openai/openai-go/v3/option" ··· 264 263 messages = append(messages, user("Default: generate 3 recipes")) 265 264 messages = append(messages, user("Default: prep and cook time under 1 hour")) 266 265 messages = append(messages, user("Default: cooking methods: oven, stove, grill, slow cooker")) 267 - //location and date for seasonal ingredientss 268 - 269 - //Available ingredients (in TOON format for token efficiency) 270 - ingredientsMessage := "Ingredients currently on sale in TOON format\n" 271 266 272 - encoded, err := gotoon.Encode(saleIngredients) 273 - if err != nil { 274 - return nil, fmt.Errorf("failed to encode ingredients to TOON: %w", err) 267 + ingredientsMessage := fmt.Sprintf("%d ingredients available in TSV format with header.\n", len(saleIngredients)) 268 + var buf strings.Builder 269 + if err := kroger.ToTSV(saleIngredients, &buf); err != nil { 270 + return nil, fmt.Errorf("failed to convert ingredients to TSV: %w", err) 275 271 } 276 - ingredientsMessage += encoded 277 - 272 + ingredientsMessage += buf.String() 278 273 messages = append(messages, user(ingredientsMessage)) 279 274 280 - // Previous recipes to avoid (if any) 275 + // Previous recipes to avoid (if any). Reduce this to already cooked? 281 276 if len(lastRecipes) > 0 { 282 277 var prevRecipesMsg strings.Builder 283 278 prevRecipesMsg.WriteString("Avoid recipes similar to these from the past 2 weeks:\n")
+64 -10
internal/kroger/type.go
··· 1 1 package kroger 2 2 3 + import ( 4 + "encoding/csv" 5 + "fmt" 6 + "io" 7 + ) 8 + 3 9 // this is a subset of ProductSearchResponse200Data combining item and product we think will be useful 4 10 // TODO merge with ai.Ingredient 5 11 type Ingredient struct { 6 - //ProductId string `json:"productId,omitempty"` 12 + ProductId *string `json:"id,omitempty"` 7 13 AisleNumber *string `json:"number,omitempty"` 8 14 Brand *string `json:"brand,omitempty"` 9 - //Categories *[]string `json:"categories,omitempty"` 10 - CountryOrigin *string `json:"countryOrigin,omitempty"` 11 - Description *string `json:"description,omitempty"` 12 - Favorite *bool `json:"favorite,omitempty"` //what does this mean? 13 - InventoryStockLevel *string `json:"stockLevel,omitempty"` 14 - PriceSale *float32 `json:"salePrice,omitempty"` 15 - PriceRegular *float32 `json:"regularPrice,omitempty"` 16 - Size *string `json:"size,omitempty"` 17 - Categories *[]string `json:"categories,omitempty"` 15 + //CountryOrigin *string `json:"countryOrigin,omitempty"` 16 + Description *string `json:"description,omitempty"` 17 + //Favorite *bool `json:"favorite,omitempty"` //what does this mean? 18 + //InventoryStockLevel *string `json:"stockLevel,omitempty"` 19 + PriceSale *float32 `json:"salePrice,omitempty"` 20 + PriceRegular *float32 `json:"regularPrice,omitempty"` 21 + Size *string `json:"size,omitempty"` 22 + //not used by llm. 23 + Categories *[]string `json:"categories,omitempty"` 18 24 //Figure out what is in taxonomies 19 25 } 26 + 27 + // this is what we'll actually pass to the llm 28 + func ToTSV(ingredient []Ingredient, w io.Writer) error { 29 + csvw := csv.NewWriter(w) 30 + csvw.Comma = '\t' 31 + header := []string{"ProductId", "AisleNumber", "Brand", "Description", "Size", "PriceRegular", "PriceSale"} 32 + if err := csvw.Write(header); err != nil { 33 + return err 34 + } 35 + for _, i := range ingredient { 36 + if i.PriceSale == nil { 37 + i.PriceSale = i.PriceRegular 38 + } 39 + row := []string{ 40 + toStr(i.ProductId), 41 + toStr(i.AisleNumber), 42 + toStr(i.Brand), 43 + toStr(i.Description), 44 + toStr(i.Size), 45 + floatToStr(i.PriceRegular), 46 + floatToStr(i.PriceSale), 47 + //todo add a dicount? 48 + } 49 + if len(header) != len(row) { 50 + return fmt.Errorf("header and row length mismatch: %d vs %d", len(header), len(row)) 51 + } 52 + if err := csvw.Write(row); err != nil { 53 + return err 54 + } 55 + } 56 + csvw.Flush() 57 + return csvw.Error() 58 + } 59 + 60 + // toStr returns the string value if non-nil, or "empty" otherwise. 61 + func floatToStr(f *float32) string { 62 + if f == nil { 63 + return "" 64 + } 65 + return fmt.Sprintf("%.2f", *f) 66 + } 67 + 68 + func toStr(s *string) string { 69 + if s == nil { 70 + return "" 71 + } 72 + return *s 73 + }
+42
internal/kroger/types_test.go
··· 1 + package kroger 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + 7 + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" 8 + ) 9 + 10 + func TestEncodeSaleIngredientsToTS(t *testing.T) { 11 + brand := "Foster Farms" 12 + description := "Fresh chicken thighs" 13 + 14 + var sb strings.Builder 15 + err := ToTSV([]Ingredient{ 16 + { 17 + ProductId: to.Ptr("444"), 18 + Brand: &brand, 19 + Description: &description, 20 + AisleNumber: to.Ptr("7"), 21 + PriceRegular: to.Ptr(float32(3.50)), // should be omitted due to omitempty 22 + PriceSale: nil, // should be omitted due to omitempty 23 + }, 24 + }, &sb) 25 + encoded := sb.String() 26 + if err != nil { 27 + t.Fatalf("unexpected encode error: %v", err) 28 + } 29 + 30 + if strings.Contains(encoded, "omitempty") { 31 + t.Fatalf("encoded payload should not contain omitempty tags: %s", encoded) 32 + } 33 + if strings.Contains(encoded, "null") { 34 + t.Fatalf("encoded payload should not include null values: %s", encoded) 35 + } 36 + if !strings.Contains(encoded, "Brand") || !strings.Contains(encoded, "Description") { 37 + t.Fatalf("encoded payload should include expected keys:\n %s", encoded) 38 + } 39 + if strings.Contains(encoded, "favorite") { 40 + t.Fatalf("encoded payload should omit nil fields with omitempty: %s", encoded) 41 + } 42 + }
+8
internal/recipes/generator.go
··· 202 202 continue 203 203 } 204 204 205 + var aisle *string 206 + if product.AisleLocations != nil && len(*product.AisleLocations) > 0 { 207 + aisle = (*product.AisleLocations)[0].Number 208 + } 209 + 205 210 // does just giving the model json work better here? 206 211 ingredient := kroger.Ingredient{ 212 + ProductId: product.ProductId, //chat gpt act 207 213 Brand: product.Brand, 208 214 Description: product.Description, 209 215 Size: item.Size, 210 216 PriceRegular: item.Price.Regular, 211 217 PriceSale: item.Price.Promo, 212 218 Categories: product.Categories, 219 + AisleNumber: aisle, 220 + 213 221 /*"taxonomies": [ 214 222 { 215 223 "department": {},
+26 -20
internal/recipes/server.go
··· 76 76 mux.HandleFunc("POST /recipe/{hash}/save", s.handleSaveRecipe) 77 77 mux.HandleFunc("POST /recipe/{hash}/dismiss", s.handleDismissRecipe) 78 78 //maybe this should be under locations server? 79 - mux.HandleFunc("GET /ingredients/{location}", s.ingredients) 79 + mux.HandleFunc("GET /ingredients/{hash}", s.ingredients) 80 80 81 81 } 82 82 ··· 842 842 // move to admin? Nah let the people see 843 843 func (s *server) ingredients(w http.ResponseWriter, r *http.Request) { 844 844 ctx := r.Context() 845 - loc := r.PathValue("location") 846 - l, err := s.locServer.GetLocationByID(ctx, loc) 845 + hash := r.PathValue("hash") 846 + p, err := s.ParamsFromCache(ctx, hash) 847 847 if err != nil { 848 - http.Error(w, "invalid location id", http.StatusBadRequest) 848 + if errors.Is(err, cache.ErrNotFound) { 849 + http.Error(w, "parameters not found in cache", http.StatusNotFound) 850 + return 851 + } 852 + slog.ErrorContext(ctx, "failed to load params for hash", "hash", hash, "error", err) 853 + http.Error(w, "failed to fetch params", http.StatusInternalServerError) 849 854 return 850 855 } 851 - // later use saved items 852 - p := DefaultParams(l, time.Now()) 853 - 854 856 lochash := p.LocationHash() 855 - ingredientblob, err := s.cache.Get(ctx, lochash) 857 + 858 + ingredients, err := s.IngredientsFromCache(ctx, lochash) 856 859 if err != nil { 857 - http.Error(w, "ingredients not found in cache", http.StatusNotFound) 860 + if errors.Is(err, cache.ErrNotFound) { 861 + http.Error(w, "ingredients not found in cache", http.StatusNotFound) 862 + return 863 + } 864 + slog.ErrorContext(ctx, "failed to load ingredients for hash", "hash", lochash, "error", err) 865 + http.Error(w, "failed to fetch ingredients", http.StatusInternalServerError) 858 866 return 859 867 } 860 868 slog.Info("serving cached ingredients", "location", p.String(), "hash", lochash) 861 - defer func() { 862 - if err := ingredientblob.Close(); err != nil { 863 - slog.ErrorContext(ctx, "failed to close cached ingredients", "location", p.String(), "error", err) 869 + // make this a html thats readable. 870 + if r.URL.Query().Get("format") == "tsv" { 871 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 872 + err := kroger.ToTSV(ingredients, w) 873 + if err != nil { 874 + http.Error(w, "failed to encode ingredients", http.StatusInternalServerError) 875 + return 864 876 } 865 - }() 866 - dec := json.NewDecoder(ingredientblob) 867 - var ingredients []kroger.Ingredient 868 - err = dec.Decode(&ingredients) 869 - if err != nil { 870 - http.Error(w, "failed to decode ingredients", http.StatusInternalServerError) 871 877 return 872 878 } 873 - // make this a html thats readable. 874 - w.Header().Add("Content-Type", "application/json") 879 + 880 + w.Header().Set("Content-Type", "application/json; charset=utf-8") 875 881 enc := json.NewEncoder(w) 876 882 enc.SetIndent("", " ") 877 883 if err := enc.Encode(ingredients); err != nil {