refactor: introduce adapter pattern for AI provider (OpenAI)

- 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>
This commit is contained in:
dbastrikin
2026-03-15 21:27:04 +02:00
parent 19a985ad49
commit fee240da7d
8 changed files with 217 additions and 194 deletions

View File

@@ -4,34 +4,14 @@ import (
"context"
"fmt"
"sync"
"github.com/food-ai/backend/internal/adapters/ai"
)
// 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) {
func (c *Client) GenerateMenu(ctx context.Context, req ai.MenuRequest) ([]ai.DayPlan, error) {
type mealSlot struct {
mealType string
fraction float64 // share of daily calories
@@ -44,7 +24,7 @@ func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan,
}
type mealResult struct {
recipes []Recipe
recipes []ai.Recipe
err error
}
@@ -57,7 +37,7 @@ func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan,
defer wg.Done()
// Scale daily calories to what this meal should contribute.
mealCal := int(float64(req.DailyCalories) * fraction)
r, err := c.GenerateRecipes(ctx, RecipeRequest{
r, err := c.GenerateRecipes(ctx, ai.RecipeRequest{
UserGoal: req.UserGoal,
DailyCalories: mealCal * 3, // prompt divides by 3 internally
Restrictions: req.Restrictions,
@@ -84,11 +64,11 @@ func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan,
}
}
days := make([]DayPlan, 7)
days := make([]ai.DayPlan, 7)
for day := range 7 {
days[day] = DayPlan{
days[day] = ai.DayPlan{
Day: day + 1,
Meals: []MealEntry{
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]},