- Add internal/adapters/ai/types.go with neutral shared types (Recipe, DayPlan, RecognizedItem, IngredientClassification, etc.) - Remove types from internal/adapters/openai/ — adapter now uses ai.* - Define Recognizer interface in recognition package - Define MenuGenerator interface in menu package - Define RecipeGenerator interface in recommendation package - Handler structs now hold interfaces, not *openai.Client - Add wire.Bind entries for the three new interface bindings To swap OpenAI for another provider: implement the three interfaces using ai.* types and change the wire.Bind lines in cmd/server/wire.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
package openai
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync"
|
||
|
||
"github.com/food-ai/backend/internal/adapters/ai"
|
||
)
|
||
|
||
// 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 ai.MenuRequest) ([]ai.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 []ai.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, ai.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([]ai.DayPlan, 7)
|
||
for day := range 7 {
|
||
days[day] = ai.DayPlan{
|
||
Day: day + 1,
|
||
Meals: []ai.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
|
||
}
|