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>
This commit is contained in:
99
backend/internal/adapters/openai/menu.go
Normal file
99
backend/internal/adapters/openai/menu.go
Normal file
@@ -0,0 +1,99 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user