ai cooking
0
fork

Configure Feed

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

way to get at it claude

Paul Miller 7073e50d 7b4a30f0

+989
+79
cmd/careme/main.go
··· 1 + package main 2 + 3 + import ( 4 + "flag" 5 + "fmt" 6 + "log" 7 + "os" 8 + 9 + "careme/internal/config" 10 + "careme/internal/recipes" 11 + ) 12 + 13 + func main() { 14 + var location string 15 + var help bool 16 + 17 + flag.StringVar(&location, "location", "", "Location for recipe sourcing (e.g., Seattle, WA)") 18 + flag.StringVar(&location, "l", "", "Location for recipe sourcing (short form)") 19 + flag.BoolVar(&help, "help", false, "Show help message") 20 + flag.BoolVar(&help, "h", false, "Show help message") 21 + flag.Parse() 22 + 23 + if help { 24 + showHelp() 25 + return 26 + } 27 + 28 + if location == "" { 29 + fmt.Println("Error: Location is required") 30 + showHelp() 31 + os.Exit(1) 32 + } 33 + 34 + if err := run(location); err != nil { 35 + log.Fatalf("Error: %v", err) 36 + } 37 + } 38 + 39 + func run(location string) error { 40 + cfg, err := config.Load() 41 + if err != nil { 42 + return fmt.Errorf("failed to load configuration: %w", err) 43 + } 44 + 45 + generator := recipes.NewGenerator(cfg) 46 + formatter := recipes.NewFormatter() 47 + 48 + fmt.Printf("🍽️ Generating 4 weekly recipes for location: %s\n", location) 49 + fmt.Println("📍 Checking available ingredients at local QFC/Fred Meyer...") 50 + fmt.Println("🌱 Using seasonal ingredient recommendations...") 51 + fmt.Println("📚 Avoiding recipes from the past 2 weeks...") 52 + fmt.Println() 53 + 54 + generatedRecipes, err := generator.GenerateWeeklyRecipes(location) 55 + if err != nil { 56 + return fmt.Errorf("failed to generate recipes: %w", err) 57 + } 58 + 59 + output := formatter.FormatRecipes(generatedRecipes) 60 + fmt.Print(output) 61 + 62 + return nil 63 + } 64 + 65 + func showHelp() { 66 + fmt.Println("Careme - Weekly Recipe Generator") 67 + fmt.Println() 68 + fmt.Println("Usage:") 69 + fmt.Println(" careme -location <location>") 70 + fmt.Println(" careme -l <location>") 71 + fmt.Println() 72 + fmt.Println("Options:") 73 + fmt.Println(" -location, -l Location for recipe sourcing (required)") 74 + fmt.Println(" -help, -h Show this help message") 75 + fmt.Println() 76 + fmt.Println("Examples:") 77 + fmt.Println(" careme -location \"Seattle, WA\"") 78 + fmt.Println(" careme -l \"Portland, OR\"") 79 + }
+3
go.mod
··· 1 + module careme 2 + 3 + go 1.24.6
+218
internal/ai/client.go
··· 1 + package ai 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + "strings" 9 + ) 10 + 11 + type Client struct { 12 + provider string 13 + apiKey string 14 + model string 15 + httpClient *http.Client 16 + } 17 + 18 + type Message struct { 19 + Role string `json:"role"` 20 + Content string `json:"content"` 21 + } 22 + 23 + type OpenAIRequest struct { 24 + Model string `json:"model"` 25 + Messages []Message `json:"messages"` 26 + MaxTokens int `json:"max_tokens"` 27 + Temperature float64 `json:"temperature"` 28 + } 29 + 30 + type OpenAIResponse struct { 31 + Choices []struct { 32 + Message Message `json:"message"` 33 + } `json:"choices"` 34 + } 35 + 36 + type AnthropicRequest struct { 37 + Model string `json:"model"` 38 + Messages []Message `json:"messages"` 39 + MaxTokens int `json:"max_tokens"` 40 + Temperature float64 `json:"temperature"` 41 + } 42 + 43 + type AnthropicResponse struct { 44 + Content []struct { 45 + Text string `json:"text"` 46 + } `json:"content"` 47 + } 48 + 49 + func NewClient(provider, apiKey, model string) *Client { 50 + return &Client{ 51 + provider: provider, 52 + apiKey: apiKey, 53 + model: model, 54 + httpClient: &http.Client{}, 55 + } 56 + } 57 + 58 + func (c *Client) GenerateRecipes(location string, availableIngredients []string, seasonalIngredients []string, previousRecipes []string) (string, error) { 59 + prompt := c.buildRecipePrompt(location, availableIngredients, seasonalIngredients, previousRecipes) 60 + 61 + messages := []Message{ 62 + { 63 + Role: "system", 64 + Content: "You are a professional chef and recipe developer. Generate exactly 4 unique, practical recipes based on the provided constraints. Format your response as JSON with an array of recipe objects, each containing: name, description, ingredients (array), and instructions (array).", 65 + }, 66 + { 67 + Role: "user", 68 + Content: prompt, 69 + }, 70 + } 71 + 72 + switch strings.ToLower(c.provider) { 73 + case "openai": 74 + return c.generateWithOpenAI(messages) 75 + case "anthropic": 76 + return c.generateWithAnthropic(messages) 77 + default: 78 + return "", fmt.Errorf("unsupported AI provider: %s", c.provider) 79 + } 80 + } 81 + 82 + func (c *Client) generateWithOpenAI(messages []Message) (string, error) { 83 + request := OpenAIRequest{ 84 + Model: c.model, 85 + Messages: messages, 86 + MaxTokens: 2000, 87 + Temperature: 0.7, 88 + } 89 + 90 + jsonData, err := json.Marshal(request) 91 + if err != nil { 92 + return "", fmt.Errorf("failed to marshal OpenAI request: %w", err) 93 + } 94 + 95 + req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(jsonData)) 96 + if err != nil { 97 + return "", fmt.Errorf("failed to create OpenAI request: %w", err) 98 + } 99 + 100 + req.Header.Set("Content-Type", "application/json") 101 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 102 + 103 + resp, err := c.httpClient.Do(req) 104 + if err != nil { 105 + return "", fmt.Errorf("failed to make OpenAI request: %w", err) 106 + } 107 + defer resp.Body.Close() 108 + 109 + if resp.StatusCode != http.StatusOK { 110 + return "", fmt.Errorf("OpenAI API request failed with status: %d", resp.StatusCode) 111 + } 112 + 113 + var openAIResp OpenAIResponse 114 + if err := json.NewDecoder(resp.Body).Decode(&openAIResp); err != nil { 115 + return "", fmt.Errorf("failed to decode OpenAI response: %w", err) 116 + } 117 + 118 + if len(openAIResp.Choices) == 0 { 119 + return "", fmt.Errorf("no choices in OpenAI response") 120 + } 121 + 122 + return openAIResp.Choices[0].Message.Content, nil 123 + } 124 + 125 + func (c *Client) generateWithAnthropic(messages []Message) (string, error) { 126 + request := AnthropicRequest{ 127 + Model: c.model, 128 + Messages: messages, 129 + MaxTokens: 2000, 130 + Temperature: 0.7, 131 + } 132 + 133 + jsonData, err := json.Marshal(request) 134 + if err != nil { 135 + return "", fmt.Errorf("failed to marshal Anthropic request: %w", err) 136 + } 137 + 138 + req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewBuffer(jsonData)) 139 + if err != nil { 140 + return "", fmt.Errorf("failed to create Anthropic request: %w", err) 141 + } 142 + 143 + req.Header.Set("Content-Type", "application/json") 144 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 145 + req.Header.Set("anthropic-version", "2023-06-01") 146 + 147 + resp, err := c.httpClient.Do(req) 148 + if err != nil { 149 + return "", fmt.Errorf("failed to make Anthropic request: %w", err) 150 + } 151 + defer resp.Body.Close() 152 + 153 + if resp.StatusCode != http.StatusOK { 154 + return "", fmt.Errorf("Anthropic API request failed with status: %d", resp.StatusCode) 155 + } 156 + 157 + var anthropicResp AnthropicResponse 158 + if err := json.NewDecoder(resp.Body).Decode(&anthropicResp); err != nil { 159 + return "", fmt.Errorf("failed to decode Anthropic response: %w", err) 160 + } 161 + 162 + if len(anthropicResp.Content) == 0 { 163 + return "", fmt.Errorf("no content in Anthropic response") 164 + } 165 + 166 + return anthropicResp.Content[0].Text, nil 167 + } 168 + 169 + func (c *Client) buildRecipePrompt(location, availableIngredients, seasonalIngredients, previousRecipes []string) string { 170 + prompt := fmt.Sprintf("Generate 4 unique weekly recipes for location: %s\n\n", location) 171 + 172 + if len(availableIngredients) > 0 { 173 + prompt += "Available fresh ingredients at local QFC/Fred Meyer:\n" 174 + for _, ingredient := range availableIngredients { 175 + prompt += fmt.Sprintf("- %s\n", ingredient) 176 + } 177 + prompt += "\n" 178 + } 179 + 180 + if len(seasonalIngredients) > 0 { 181 + prompt += "Seasonal ingredients currently available:\n" 182 + for _, ingredient := range seasonalIngredients { 183 + prompt += fmt.Sprintf("- %s\n", ingredient) 184 + } 185 + prompt += "\n" 186 + } 187 + 188 + if len(previousRecipes) > 0 { 189 + prompt += "DO NOT repeat these recipes from the past 2 weeks:\n" 190 + for _, recipe := range previousRecipes { 191 + prompt += fmt.Sprintf("- %s\n", recipe) 192 + } 193 + prompt += "\n" 194 + } 195 + 196 + prompt += "Requirements:\n" 197 + prompt += "- Generate exactly 4 recipes\n" 198 + prompt += "- Prioritize available fresh ingredients\n" 199 + prompt += "- Use seasonal ingredients when possible\n" 200 + prompt += "- Avoid repeating previous recipes\n" 201 + prompt += "- Include variety in cooking methods and cuisines\n" 202 + prompt += "- Each recipe should serve 4 people\n" 203 + prompt += "- Provide clear, step-by-step instructions\n\n" 204 + 205 + prompt += "Format your response as valid JSON with this structure:\n" 206 + prompt += `{ 207 + "recipes": [ 208 + { 209 + "name": "Recipe Name", 210 + "description": "Brief description", 211 + "ingredients": ["ingredient 1", "ingredient 2"], 212 + "instructions": ["step 1", "step 2"] 213 + } 214 + ] 215 + }` 216 + 217 + return prompt 218 + }
+64
internal/config/config.go
··· 1 + package config 2 + 3 + import ( 4 + "os" 5 + ) 6 + 7 + type Config struct { 8 + AI AIConfig `json:"ai"` 9 + Kroger KrogerConfig `json:"kroger"` 10 + Epicurious EpicuriousConfig `json:"epicurious"` 11 + History HistoryConfig `json:"history"` 12 + } 13 + 14 + type AIConfig struct { 15 + Provider string `json:"provider"` // "openai" or "anthropic" 16 + APIKey string `json:"api_key"` 17 + Model string `json:"model"` 18 + } 19 + 20 + type KrogerConfig struct { 21 + MCPServerURL string `json:"mcp_server_url"` 22 + APIKey string `json:"api_key"` 23 + } 24 + 25 + type EpicuriousConfig struct { 26 + APIEndpoint string `json:"api_endpoint"` 27 + APIKey string `json:"api_key"` 28 + } 29 + 30 + type HistoryConfig struct { 31 + StoragePath string `json:"storage_path"` 32 + RetentionDays int `json:"retention_days"` 33 + } 34 + 35 + func Load() (*Config, error) { 36 + config := &Config{ 37 + AI: AIConfig{ 38 + Provider: getEnvOrDefault("AI_PROVIDER", "openai"), 39 + APIKey: os.Getenv("AI_API_KEY"), 40 + Model: getEnvOrDefault("AI_MODEL", "gpt-4"), 41 + }, 42 + Kroger: KrogerConfig{ 43 + MCPServerURL: getEnvOrDefault("KROGER_MCP_URL", "http://localhost:8080"), 44 + APIKey: os.Getenv("KROGER_API_KEY"), 45 + }, 46 + Epicurious: EpicuriousConfig{ 47 + APIEndpoint: getEnvOrDefault("EPICURIOUS_ENDPOINT", "https://api.epicurious.com"), 48 + APIKey: os.Getenv("EPICURIOUS_API_KEY"), 49 + }, 50 + History: HistoryConfig{ 51 + StoragePath: getEnvOrDefault("HISTORY_PATH", "./data/history.json"), 52 + RetentionDays: 14, 53 + }, 54 + } 55 + 56 + return config, nil 57 + } 58 + 59 + func getEnvOrDefault(key, defaultValue string) string { 60 + if value := os.Getenv(key); value != "" { 61 + return value 62 + } 63 + return defaultValue 64 + }
+162
internal/history/storage.go
··· 1 + package history 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io/ioutil" 7 + "os" 8 + "path/filepath" 9 + "time" 10 + ) 11 + 12 + type Recipe struct { 13 + ID string `json:"id"` 14 + Name string `json:"name"` 15 + Description string `json:"description"` 16 + Ingredients []string `json:"ingredients"` 17 + Instructions []string `json:"instructions"` 18 + CreatedAt time.Time `json:"created_at"` 19 + Location string `json:"location"` 20 + Season string `json:"season"` 21 + } 22 + 23 + type HistoryStorage struct { 24 + storagePath string 25 + retentionDays int 26 + } 27 + 28 + type History struct { 29 + Recipes []Recipe `json:"recipes"` 30 + } 31 + 32 + func NewHistoryStorage(storagePath string, retentionDays int) *HistoryStorage { 33 + return &HistoryStorage{ 34 + storagePath: storagePath, 35 + retentionDays: retentionDays, 36 + } 37 + } 38 + 39 + func (hs *HistoryStorage) SaveRecipes(recipes []Recipe) error { 40 + history, err := hs.loadHistory() 41 + if err != nil { 42 + return fmt.Errorf("failed to load existing history: %w", err) 43 + } 44 + 45 + for _, recipe := range recipes { 46 + recipe.CreatedAt = time.Now() 47 + history.Recipes = append(history.Recipes, recipe) 48 + } 49 + 50 + hs.cleanOldRecipes(&history) 51 + 52 + return hs.saveHistory(history) 53 + } 54 + 55 + func (hs *HistoryStorage) GetRecentRecipes(days int) ([]Recipe, error) { 56 + history, err := hs.loadHistory() 57 + if err != nil { 58 + return nil, fmt.Errorf("failed to load history: %w", err) 59 + } 60 + 61 + cutoff := time.Now().AddDate(0, 0, -days) 62 + var recentRecipes []Recipe 63 + 64 + for _, recipe := range history.Recipes { 65 + if recipe.CreatedAt.After(cutoff) { 66 + recentRecipes = append(recentRecipes, recipe) 67 + } 68 + } 69 + 70 + return recentRecipes, nil 71 + } 72 + 73 + func (hs *HistoryStorage) GetRecipeNames(days int) ([]string, error) { 74 + recipes, err := hs.GetRecentRecipes(days) 75 + if err != nil { 76 + return nil, err 77 + } 78 + 79 + var names []string 80 + for _, recipe := range recipes { 81 + names = append(names, recipe.Name) 82 + } 83 + 84 + return names, nil 85 + } 86 + 87 + func (hs *HistoryStorage) HasRecipe(recipeName string, days int) (bool, error) { 88 + names, err := hs.GetRecipeNames(days) 89 + if err != nil { 90 + return false, err 91 + } 92 + 93 + for _, name := range names { 94 + if name == recipeName { 95 + return true, nil 96 + } 97 + } 98 + 99 + return false, nil 100 + } 101 + 102 + func (hs *HistoryStorage) loadHistory() (History, error) { 103 + var history History 104 + 105 + if err := hs.ensureStorageDir(); err != nil { 106 + return history, err 107 + } 108 + 109 + if _, err := os.Stat(hs.storagePath); os.IsNotExist(err) { 110 + return history, nil 111 + } 112 + 113 + data, err := ioutil.ReadFile(hs.storagePath) 114 + if err != nil { 115 + return history, fmt.Errorf("failed to read history file: %w", err) 116 + } 117 + 118 + if err := json.Unmarshal(data, &history); err != nil { 119 + return history, fmt.Errorf("failed to unmarshal history: %w", err) 120 + } 121 + 122 + return history, nil 123 + } 124 + 125 + func (hs *HistoryStorage) saveHistory(history History) error { 126 + if err := hs.ensureStorageDir(); err != nil { 127 + return err 128 + } 129 + 130 + data, err := json.MarshalIndent(history, "", " ") 131 + if err != nil { 132 + return fmt.Errorf("failed to marshal history: %w", err) 133 + } 134 + 135 + if err := ioutil.WriteFile(hs.storagePath, data, 0644); err != nil { 136 + return fmt.Errorf("failed to write history file: %w", err) 137 + } 138 + 139 + return nil 140 + } 141 + 142 + func (hs *HistoryStorage) ensureStorageDir() error { 143 + dir := filepath.Dir(hs.storagePath) 144 + return os.MkdirAll(dir, 0755) 145 + } 146 + 147 + func (hs *HistoryStorage) cleanOldRecipes(history *History) { 148 + if hs.retentionDays <= 0 { 149 + return 150 + } 151 + 152 + cutoff := time.Now().AddDate(0, 0, -hs.retentionDays) 153 + var keepRecipes []Recipe 154 + 155 + for _, recipe := range history.Recipes { 156 + if recipe.CreatedAt.After(cutoff) { 157 + keepRecipes = append(keepRecipes, recipe) 158 + } 159 + } 160 + 161 + history.Recipes = keepRecipes 162 + }
+133
internal/ingredients/seasonal.go
··· 1 + package ingredients 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + "time" 9 + ) 10 + 11 + type SeasonalClient struct { 12 + apiEndpoint string 13 + apiKey string 14 + httpClient *http.Client 15 + } 16 + 17 + type SeasonalIngredient struct { 18 + Name string `json:"name"` 19 + Season []string `json:"season"` 20 + Peak []string `json:"peak"` 21 + Category string `json:"category"` 22 + Description string `json:"description"` 23 + Substitutes []string `json:"substitutes"` 24 + } 25 + 26 + type SeasonalResponse struct { 27 + Ingredients []SeasonalIngredient `json:"ingredients"` 28 + Season string `json:"current_season"` 29 + Region string `json:"region"` 30 + } 31 + 32 + func NewSeasonalClient(apiEndpoint, apiKey string) *SeasonalClient { 33 + return &SeasonalClient{ 34 + apiEndpoint: apiEndpoint, 35 + apiKey: apiKey, 36 + httpClient: &http.Client{Timeout: 30 * time.Second}, 37 + } 38 + } 39 + 40 + func (c *SeasonalClient) GetSeasonalIngredients(location string) ([]SeasonalIngredient, error) { 41 + season := getCurrentSeason() 42 + url := fmt.Sprintf("%s/seasonal?location=%s&season=%s", c.apiEndpoint, location, season) 43 + 44 + req, err := http.NewRequest("GET", url, nil) 45 + if err != nil { 46 + return nil, fmt.Errorf("failed to create request: %w", err) 47 + } 48 + 49 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 50 + req.Header.Set("Accept", "application/json") 51 + 52 + resp, err := c.httpClient.Do(req) 53 + if err != nil { 54 + return nil, fmt.Errorf("failed to make request: %w", err) 55 + } 56 + defer resp.Body.Close() 57 + 58 + if resp.StatusCode != http.StatusOK { 59 + return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) 60 + } 61 + 62 + var seasonalResp SeasonalResponse 63 + if err := json.NewDecoder(resp.Body).Decode(&seasonalResp); err != nil { 64 + return nil, fmt.Errorf("failed to decode response: %w", err) 65 + } 66 + 67 + return seasonalResp.Ingredients, nil 68 + } 69 + 70 + func (c *SeasonalClient) GetIngredientsForSeason(season, location string) ([]SeasonalIngredient, error) { 71 + url := fmt.Sprintf("%s/seasonal?location=%s&season=%s", c.apiEndpoint, location, season) 72 + 73 + req, err := http.NewRequest("GET", url, nil) 74 + if err != nil { 75 + return nil, fmt.Errorf("failed to create request: %w", err) 76 + } 77 + 78 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 79 + req.Header.Set("Accept", "application/json") 80 + 81 + resp, err := c.httpClient.Do(req) 82 + if err != nil { 83 + return nil, fmt.Errorf("failed to make request: %w", err) 84 + } 85 + defer resp.Body.Close() 86 + 87 + if resp.StatusCode != http.StatusOK { 88 + return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) 89 + } 90 + 91 + var seasonalResp SeasonalResponse 92 + if err := json.NewDecoder(resp.Body).Decode(&seasonalResp); err != nil { 93 + return nil, fmt.Errorf("failed to decode response: %w", err) 94 + } 95 + 96 + return seasonalResp.Ingredients, nil 97 + } 98 + 99 + func getCurrentSeason() string { 100 + now := time.Now() 101 + month := now.Month() 102 + 103 + switch { 104 + case month >= time.March && month <= time.May: 105 + return "spring" 106 + case month >= time.June && month <= time.August: 107 + return "summer" 108 + case month >= time.September && month <= time.November: 109 + return "fall" 110 + default: 111 + return "winter" 112 + } 113 + } 114 + 115 + func (s *SeasonalIngredient) IsInSeason(season string) bool { 116 + season = strings.ToLower(season) 117 + for _, s := range s.Season { 118 + if strings.ToLower(s) == season { 119 + return true 120 + } 121 + } 122 + return false 123 + } 124 + 125 + func (s *SeasonalIngredient) IsAtPeak(season string) bool { 126 + season = strings.ToLower(season) 127 + for _, p := range s.Peak { 128 + if strings.ToLower(p) == season { 129 + return true 130 + } 131 + } 132 + return false 133 + }
+87
internal/kroger/client.go
··· 1 + package kroger 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "net/http" 8 + ) 9 + 10 + type Client struct { 11 + mcpServerURL string 12 + apiKey string 13 + httpClient *http.Client 14 + } 15 + 16 + type Product struct { 17 + ID string `json:"id"` 18 + Name string `json:"name"` 19 + Brand string `json:"brand"` 20 + Price float64 `json:"price"` 21 + Available bool `json:"available"` 22 + Fresh bool `json:"fresh"` 23 + Category string `json:"category"` 24 + Description string `json:"description"` 25 + } 26 + 27 + type SearchRequest struct { 28 + Location string `json:"location"` 29 + Keywords []string `json:"keywords"` 30 + Categories []string `json:"categories"` 31 + FreshOnly bool `json:"fresh_only"` 32 + } 33 + 34 + type SearchResponse struct { 35 + Products []Product `json:"products"` 36 + Total int `json:"total"` 37 + } 38 + 39 + func NewClient(mcpServerURL, apiKey string) *Client { 40 + return &Client{ 41 + mcpServerURL: mcpServerURL, 42 + apiKey: apiKey, 43 + httpClient: &http.Client{}, 44 + } 45 + } 46 + 47 + func (c *Client) SearchProducts(location string, keywords []string, freshOnly bool) ([]Product, error) { 48 + request := SearchRequest{ 49 + Location: location, 50 + Keywords: keywords, 51 + FreshOnly: freshOnly, 52 + } 53 + 54 + jsonData, err := json.Marshal(request) 55 + if err != nil { 56 + return nil, fmt.Errorf("failed to marshal request: %w", err) 57 + } 58 + 59 + req, err := http.NewRequest("POST", c.mcpServerURL+"/search", bytes.NewBuffer(jsonData)) 60 + if err != nil { 61 + return nil, fmt.Errorf("failed to create request: %w", err) 62 + } 63 + 64 + req.Header.Set("Content-Type", "application/json") 65 + req.Header.Set("Authorization", "Bearer "+c.apiKey) 66 + 67 + resp, err := c.httpClient.Do(req) 68 + if err != nil { 69 + return nil, fmt.Errorf("failed to make request: %w", err) 70 + } 71 + defer resp.Body.Close() 72 + 73 + if resp.StatusCode != http.StatusOK { 74 + return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) 75 + } 76 + 77 + var searchResp SearchResponse 78 + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { 79 + return nil, fmt.Errorf("failed to decode response: %w", err) 80 + } 81 + 82 + return searchResp.Products, nil 83 + } 84 + 85 + func (c *Client) GetFreshIngredients(location string, ingredients []string) ([]Product, error) { 86 + return c.SearchProducts(location, ingredients, true) 87 + }
+62
internal/recipes/formatter.go
··· 1 + package recipes 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + "careme/internal/history" 8 + ) 9 + 10 + type Formatter struct{} 11 + 12 + func NewFormatter() *Formatter { 13 + return &Formatter{} 14 + } 15 + 16 + func (f *Formatter) FormatRecipes(recipes []history.Recipe) string { 17 + var output strings.Builder 18 + 19 + output.WriteString("🍽️ CAREME WEEKLY RECIPES\n") 20 + output.WriteString(strings.Repeat("=", 50) + "\n\n") 21 + 22 + for i, recipe := range recipes { 23 + output.WriteString(fmt.Sprintf("📋 RECIPE %d: %s\n", i+1, strings.ToUpper(recipe.Name))) 24 + output.WriteString(strings.Repeat("-", 30) + "\n") 25 + 26 + if recipe.Description != "" { 27 + output.WriteString(fmt.Sprintf("Description: %s\n\n", recipe.Description)) 28 + } 29 + 30 + output.WriteString("🛒 INGREDIENTS:\n") 31 + for _, ingredient := range recipe.Ingredients { 32 + output.WriteString(fmt.Sprintf(" • %s\n", ingredient)) 33 + } 34 + output.WriteString("\n") 35 + 36 + output.WriteString("👩‍🍳 INSTRUCTIONS:\n") 37 + for j, instruction := range recipe.Instructions { 38 + output.WriteString(fmt.Sprintf(" %d. %s\n", j+1, instruction)) 39 + } 40 + 41 + if i < len(recipes)-1 { 42 + output.WriteString("\n" + strings.Repeat("=", 50) + "\n\n") 43 + } 44 + } 45 + 46 + output.WriteString("\n" + strings.Repeat("=", 50) + "\n") 47 + output.WriteString("🎯 Generated with fresh, seasonal ingredients!\n") 48 + output.WriteString("📍 Sourced from your local QFC/Fred Meyer\n") 49 + 50 + return output.String() 51 + } 52 + 53 + func (f *Formatter) FormatRecipeList(recipes []history.Recipe) string { 54 + var output strings.Builder 55 + 56 + output.WriteString("📋 This Week's Recipes:\n") 57 + for i, recipe := range recipes { 58 + output.WriteString(fmt.Sprintf(" %d. %s\n", i+1, recipe.Name)) 59 + } 60 + 61 + return output.String() 62 + }
+137
internal/recipes/generator.go
··· 1 + package recipes 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "log" 7 + 8 + "careme/internal/ai" 9 + "careme/internal/config" 10 + "careme/internal/history" 11 + "careme/internal/ingredients" 12 + "careme/internal/kroger" 13 + ) 14 + 15 + type Generator struct { 16 + config *config.Config 17 + aiClient *ai.Client 18 + krogerClient *kroger.Client 19 + seasonalClient *ingredients.SeasonalClient 20 + historyStorage *history.HistoryStorage 21 + } 22 + 23 + type GeneratedRecipes struct { 24 + Recipes []history.Recipe `json:"recipes"` 25 + } 26 + 27 + func NewGenerator(cfg *config.Config) *Generator { 28 + return &Generator{ 29 + config: cfg, 30 + aiClient: ai.NewClient(cfg.AI.Provider, cfg.AI.APIKey, cfg.AI.Model), 31 + krogerClient: kroger.NewClient(cfg.Kroger.MCPServerURL, cfg.Kroger.APIKey), 32 + seasonalClient: ingredients.NewSeasonalClient(cfg.Epicurious.APIEndpoint, cfg.Epicurious.APIKey), 33 + historyStorage: history.NewHistoryStorage(cfg.History.StoragePath, cfg.History.RetentionDays), 34 + } 35 + } 36 + 37 + func (g *Generator) GenerateWeeklyRecipes(location string) ([]history.Recipe, error) { 38 + log.Printf("Generating recipes for location: %s", location) 39 + 40 + availableIngredients, err := g.getAvailableIngredients(location) 41 + if err != nil { 42 + log.Printf("Warning: Could not fetch available ingredients: %v", err) 43 + availableIngredients = []string{} 44 + } 45 + 46 + seasonalIngredients, err := g.getSeasonalIngredients(location) 47 + if err != nil { 48 + log.Printf("Warning: Could not fetch seasonal ingredients: %v", err) 49 + seasonalIngredients = []string{} 50 + } 51 + 52 + previousRecipes, err := g.getPreviousRecipes() 53 + if err != nil { 54 + log.Printf("Warning: Could not fetch recipe history: %v", err) 55 + previousRecipes = []string{} 56 + } 57 + 58 + log.Printf("Found %d available ingredients, %d seasonal ingredients, %d previous recipes", 59 + len(availableIngredients), len(seasonalIngredients), len(previousRecipes)) 60 + 61 + response, err := g.aiClient.GenerateRecipes(location, availableIngredients, seasonalIngredients, previousRecipes) 62 + if err != nil { 63 + return nil, fmt.Errorf("failed to generate recipes with AI: %w", err) 64 + } 65 + 66 + recipes, err := g.parseAIResponse(response, location) 67 + if err != nil { 68 + return nil, fmt.Errorf("failed to parse AI response: %w", err) 69 + } 70 + 71 + if err := g.historyStorage.SaveRecipes(recipes); err != nil { 72 + log.Printf("Warning: Could not save recipes to history: %v", err) 73 + } 74 + 75 + return recipes, nil 76 + } 77 + 78 + func (g *Generator) getAvailableIngredients(location string) ([]string, error) { 79 + commonIngredients := []string{ 80 + "chicken", "beef", "pork", "salmon", "eggs", 81 + "onions", "garlic", "potatoes", "carrots", "celery", 82 + "tomatoes", "bell peppers", "broccoli", "spinach", "lettuce", 83 + "rice", "pasta", "bread", "milk", "cheese", "butter", 84 + } 85 + 86 + products, err := g.krogerClient.GetFreshIngredients(location, commonIngredients) 87 + if err != nil { 88 + return nil, err 89 + } 90 + 91 + var available []string 92 + for _, product := range products { 93 + if product.Available && product.Fresh { 94 + available = append(available, product.Name) 95 + } 96 + } 97 + 98 + return available, nil 99 + } 100 + 101 + func (g *Generator) getSeasonalIngredients(location string) ([]string, error) { 102 + seasonalItems, err := g.seasonalClient.GetSeasonalIngredients(location) 103 + if err != nil { 104 + return nil, err 105 + } 106 + 107 + var ingredients []string 108 + for _, item := range seasonalItems { 109 + ingredients = append(ingredients, item.Name) 110 + } 111 + 112 + return ingredients, nil 113 + } 114 + 115 + func (g *Generator) getPreviousRecipes() ([]string, error) { 116 + return g.historyStorage.GetRecipeNames(14) // Last 2 weeks 117 + } 118 + 119 + func (g *Generator) parseAIResponse(response, location string) ([]history.Recipe, error) { 120 + var generatedRecipes GeneratedRecipes 121 + if err := json.Unmarshal([]byte(response), &generatedRecipes); err != nil { 122 + return nil, fmt.Errorf("failed to unmarshal AI response: %w", err) 123 + } 124 + 125 + if len(generatedRecipes.Recipes) != 4 { 126 + return nil, fmt.Errorf("expected 4 recipes, got %d", len(generatedRecipes.Recipes)) 127 + } 128 + 129 + var recipes []history.Recipe 130 + for i, recipe := range generatedRecipes.Recipes { 131 + recipe.ID = fmt.Sprintf("recipe_%d", i+1) 132 + recipe.Location = location 133 + recipes = append(recipes, recipe) 134 + } 135 + 136 + return recipes, nil 137 + }
+44
todo.md
··· 1 + # Careme TODO List 2 + 3 + ## High Priority 4 + - [x] Create todo.md file with project tasks 5 + - [ ] Initialize Go module and create basic project structure 6 + - [ ] Create main.go with basic CLI input handling for location 7 + - [ ] Set up configuration structure for AI models (ChatGPT/Anthropic) 8 + 9 + ## Medium Priority 10 + - [ ] Create package structure for MCP server integration (Kroger API) 11 + - [ ] Create package for Epicurious Seasonal Ingredient Map integration 12 + - [ ] Create recipe history storage and retrieval system 13 + - [ ] Implement recipe generation logic using AI models 14 + 15 + ## Low Priority 16 + - [ ] Create recipe output formatting and display 17 + - [ ] Add error handling and logging throughout the application 18 + 19 + ## Project Structure Overview 20 + ``` 21 + careme/ 22 + ├── cmd/ 23 + │ └── careme/ 24 + │ └── main.go 25 + ├── internal/ 26 + │ ├── config/ 27 + │ ├── kroger/ 28 + │ ├── ingredients/ 29 + │ ├── history/ 30 + │ ├── ai/ 31 + │ └── recipes/ 32 + ├── pkg/ 33 + ├── go.mod 34 + ├── go.sum 35 + └── README.md 36 + ``` 37 + 38 + ## Notes 39 + - Application takes location as input 40 + - Generates 4 recipes per week 41 + - Uses ChatGPT or Anthropic models 42 + - Integrates with Kroger API via MCP server 43 + - Uses Epicurious Seasonal Ingredient Map 44 + - Maintains 2-week recipe history to avoid repetition