Files
food-ai/backend/internal/adapters/openai/menu.go
dbastrikin 19a985ad49 refactor: restructure internal/ into adapters/, infra/, and app layers
- internal/gemini/ → internal/adapters/openai/ (renamed package to openai)
- internal/pexels/ → internal/adapters/pexels/
- internal/config/   → internal/infra/config/
- internal/database/ → internal/infra/database/
- internal/locale/   → internal/infra/locale/
- internal/middleware/ → internal/infra/middleware/
- internal/server/   → internal/infra/server/

All import paths and call sites updated accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:10:37 +02:00

100 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package openai
import (
"context"
"fmt"
"sync"
)
// MenuRequest contains parameters for weekly menu generation.
type MenuRequest struct {
UserGoal string
DailyCalories int
Restrictions []string
CuisinePrefs []string
AvailableProducts []string
Lang string // ISO 639-1 target language code, e.g. "en", "ru"
}
// DayPlan is the AI-generated plan for a single day.
type DayPlan struct {
Day int `json:"day"`
Meals []MealEntry `json:"meals"`
}
// MealEntry is a single meal within a day plan.
type MealEntry struct {
MealType string `json:"meal_type"` // breakfast | lunch | dinner
Recipe Recipe `json:"recipe"`
}
// GenerateMenu produces a 7-day × 3-meal plan by issuing three parallel
// GenerateRecipes calls (one per meal type). This avoids token-limit errors
// that arise from requesting 21 full recipes in a single prompt.
func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan, error) {
type mealSlot struct {
mealType string
fraction float64 // share of daily calories
}
slots := []mealSlot{
{"breakfast", 0.25},
{"lunch", 0.40},
{"dinner", 0.35},
}
type mealResult struct {
recipes []Recipe
err error
}
results := make([]mealResult, len(slots))
var wg sync.WaitGroup
for i, slot := range slots {
wg.Add(1)
go func(idx int, mealType string, fraction float64) {
defer wg.Done()
// Scale daily calories to what this meal should contribute.
mealCal := int(float64(req.DailyCalories) * fraction)
r, err := c.GenerateRecipes(ctx, RecipeRequest{
UserGoal: req.UserGoal,
DailyCalories: mealCal * 3, // prompt divides by 3 internally
Restrictions: req.Restrictions,
CuisinePrefs: req.CuisinePrefs,
Count: 7,
AvailableProducts: req.AvailableProducts,
Lang: req.Lang,
})
results[idx] = mealResult{r, err}
}(i, slot.mealType, slot.fraction)
}
wg.Wait()
for i, res := range results {
if res.err != nil {
return nil, fmt.Errorf("generate %s: %w", slots[i].mealType, res.err)
}
if len(res.recipes) == 0 {
return nil, fmt.Errorf("no %s recipes returned", slots[i].mealType)
}
// Pad to exactly 7 by repeating the last recipe.
for len(results[i].recipes) < 7 {
results[i].recipes = append(results[i].recipes, results[i].recipes[len(results[i].recipes)-1])
}
}
days := make([]DayPlan, 7)
for day := range 7 {
days[day] = DayPlan{
Day: day + 1,
Meals: []MealEntry{
{MealType: slots[0].mealType, Recipe: results[0].recipes[day]},
{MealType: slots[1].mealType, Recipe: results[1].recipes[day]},
{MealType: slots[2].mealType, Recipe: results[2].recipes[day]},
},
}
}
return days, nil
}