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

@@ -10,8 +10,8 @@ import (
"sync"
"time"
"github.com/food-ai/backend/internal/adapters/ai"
"github.com/food-ai/backend/internal/dish"
"github.com/food-ai/backend/internal/adapters/openai"
"github.com/food-ai/backend/internal/infra/locale"
"github.com/food-ai/backend/internal/infra/middleware"
"github.com/food-ai/backend/internal/user"
@@ -38,10 +38,15 @@ type RecipeSaver interface {
Create(ctx context.Context, req dish.CreateRequest) (string, error)
}
// MenuGenerator generates a 7-day meal plan via an AI provider.
type MenuGenerator interface {
GenerateMenu(ctx context.Context, req ai.MenuRequest) ([]ai.DayPlan, error)
}
// Handler handles menu and shopping-list endpoints.
type Handler struct {
repo *Repository
openaiClient *openai.Client
menuGenerator MenuGenerator
pexels PhotoSearcher
userLoader UserLoader
productLister ProductLister
@@ -51,7 +56,7 @@ type Handler struct {
// NewHandler creates a new Handler.
func NewHandler(
repo *Repository,
openaiClient *openai.Client,
menuGenerator MenuGenerator,
pexels PhotoSearcher,
userLoader UserLoader,
productLister ProductLister,
@@ -59,7 +64,7 @@ func NewHandler(
) *Handler {
return &Handler{
repo: repo,
openaiClient: openaiClient,
menuGenerator: menuGenerator,
pexels: pexels,
userLoader: userLoader,
productLister: productLister,
@@ -137,7 +142,7 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
}
// Generate 7-day plan via Gemini.
days, err := h.openaiClient.GenerateMenu(r.Context(), menuReq)
days, err := h.menuGenerator.GenerateMenu(r.Context(), menuReq)
if err != nil {
slog.Error("generate menu", "user_id", userID, "err", err)
writeError(w, http.StatusServiceUnavailable, "menu generation failed, please try again")
@@ -449,8 +454,8 @@ type userPreferences struct {
Restrictions []string `json:"restrictions"`
}
func buildMenuRequest(u *user.User, lang string) openai.MenuRequest {
req := openai.MenuRequest{DailyCalories: 2000, Lang: lang}
func buildMenuRequest(u *user.User, lang string) ai.MenuRequest {
req := ai.MenuRequest{DailyCalories: 2000, Lang: lang}
if u.Goal != nil {
req.UserGoal = *u.Goal
}
@@ -468,7 +473,7 @@ func buildMenuRequest(u *user.User, lang string) openai.MenuRequest {
}
// recipeToCreateRequest converts a Gemini recipe to a dish.CreateRequest.
func recipeToCreateRequest(r openai.Recipe) dish.CreateRequest {
func recipeToCreateRequest(r ai.Recipe) dish.CreateRequest {
cr := dish.CreateRequest{
Name: r.Title,
Description: r.Description,