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

@@ -8,7 +8,7 @@ import (
"strconv"
"sync"
"github.com/food-ai/backend/internal/adapters/openai"
"github.com/food-ai/backend/internal/adapters/ai"
"github.com/food-ai/backend/internal/infra/locale"
"github.com/food-ai/backend/internal/infra/middleware"
"github.com/food-ai/backend/internal/user"
@@ -35,21 +35,26 @@ type userPreferences struct {
Restrictions []string `json:"restrictions"`
}
// RecipeGenerator generates recipe recommendations via an AI provider.
type RecipeGenerator interface {
GenerateRecipes(ctx context.Context, req ai.RecipeRequest) ([]ai.Recipe, error)
}
// Handler handles GET /recommendations.
type Handler struct {
openaiClient *openai.Client
pexels PhotoSearcher
userLoader UserLoader
productLister ProductLister
recipeGenerator RecipeGenerator
pexels PhotoSearcher
userLoader UserLoader
productLister ProductLister
}
// NewHandler creates a new Handler.
func NewHandler(openaiClient *openai.Client, pexels PhotoSearcher, userLoader UserLoader, productLister ProductLister) *Handler {
func NewHandler(recipeGenerator RecipeGenerator, pexels PhotoSearcher, userLoader UserLoader, productLister ProductLister) *Handler {
return &Handler{
openaiClient: openaiClient,
pexels: pexels,
userLoader: userLoader,
productLister: productLister,
recipeGenerator: recipeGenerator,
pexels: pexels,
userLoader: userLoader,
productLister: productLister,
}
}
@@ -84,7 +89,7 @@ func (h *Handler) GetRecommendations(w http.ResponseWriter, r *http.Request) {
slog.Warn("load products for recommendations", "user_id", userID, "err", err)
}
recipes, err := h.openaiClient.GenerateRecipes(r.Context(), req)
recipes, err := h.recipeGenerator.GenerateRecipes(r.Context(), req)
if err != nil {
slog.Error("generate recipes", "user_id", userID, "err", err)
writeErrorJSON(w, http.StatusServiceUnavailable, "recipe generation failed, please try again")
@@ -109,8 +114,8 @@ func (h *Handler) GetRecommendations(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, recipes)
}
func buildRecipeRequest(u *user.User, count int, lang string) openai.RecipeRequest {
req := openai.RecipeRequest{
func buildRecipeRequest(u *user.User, count int, lang string) ai.RecipeRequest {
req := ai.RecipeRequest{
Count: count,
DailyCalories: 2000, // sensible default
Lang: lang,