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:
@@ -1,68 +1,16 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"context"
|
||||
|
||||
"github.com/food-ai/backend/internal/adapters/ai"
|
||||
)
|
||||
|
||||
// RecognizedItem is a food item identified in an image.
|
||||
type RecognizedItem struct {
|
||||
Name string `json:"name"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
Unit string `json:"unit"`
|
||||
Category string `json:"category"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
}
|
||||
|
||||
// UnrecognizedItem is text from a receipt that could not be identified as food.
|
||||
type UnrecognizedItem struct {
|
||||
RawText string `json:"raw_text"`
|
||||
Price float64 `json:"price,omitempty"`
|
||||
}
|
||||
|
||||
// ReceiptResult is the full result of receipt OCR.
|
||||
type ReceiptResult struct {
|
||||
Items []RecognizedItem `json:"items"`
|
||||
Unrecognized []UnrecognizedItem `json:"unrecognized"`
|
||||
}
|
||||
|
||||
// DishResult is the result of dish recognition.
|
||||
type DishResult struct {
|
||||
DishName string `json:"dish_name"`
|
||||
WeightGrams int `json:"weight_grams"`
|
||||
Calories float64 `json:"calories"`
|
||||
ProteinG float64 `json:"protein_g"`
|
||||
FatG float64 `json:"fat_g"`
|
||||
CarbsG float64 `json:"carbs_g"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
SimilarDishes []string `json:"similar_dishes"`
|
||||
}
|
||||
|
||||
// IngredientTranslation holds the localized name and aliases for one language.
|
||||
type IngredientTranslation struct {
|
||||
Lang string `json:"lang"`
|
||||
Name string `json:"name"`
|
||||
Aliases []string `json:"aliases"`
|
||||
}
|
||||
|
||||
// IngredientClassification is the AI-produced classification of an unknown food item.
|
||||
type IngredientClassification struct {
|
||||
CanonicalName string `json:"canonical_name"`
|
||||
Aliases []string `json:"aliases"` // English aliases
|
||||
Translations []IngredientTranslation `json:"translations"` // other languages
|
||||
Category string `json:"category"`
|
||||
DefaultUnit string `json:"default_unit"`
|
||||
CaloriesPer100g *float64 `json:"calories_per_100g"`
|
||||
ProteinPer100g *float64 `json:"protein_per_100g"`
|
||||
FatPer100g *float64 `json:"fat_per_100g"`
|
||||
CarbsPer100g *float64 `json:"carbs_per_100g"`
|
||||
StorageDays int `json:"storage_days"`
|
||||
}
|
||||
|
||||
// RecognizeReceipt uses the vision model to extract food items from a receipt photo.
|
||||
func (c *Client) RecognizeReceipt(ctx context.Context, imageBase64, mimeType string) (*ReceiptResult, error) {
|
||||
func (c *Client) RecognizeReceipt(ctx context.Context, imageBase64, mimeType string) (*ai.ReceiptResult, error) {
|
||||
prompt := `Ты — OCR-система для чеков из продуктовых магазинов.
|
||||
|
||||
Проанализируй фото чека и извлеки список продуктов питания.
|
||||
@@ -91,21 +39,21 @@ func (c *Client) RecognizeReceipt(ctx context.Context, imageBase64, mimeType str
|
||||
return nil, fmt.Errorf("recognize receipt: %w", err)
|
||||
}
|
||||
|
||||
var result ReceiptResult
|
||||
var result ai.ReceiptResult
|
||||
if err := parseJSON(text, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse receipt result: %w", err)
|
||||
}
|
||||
if result.Items == nil {
|
||||
result.Items = []RecognizedItem{}
|
||||
result.Items = []ai.RecognizedItem{}
|
||||
}
|
||||
if result.Unrecognized == nil {
|
||||
result.Unrecognized = []UnrecognizedItem{}
|
||||
result.Unrecognized = []ai.UnrecognizedItem{}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RecognizeProducts uses the vision model to identify food items in a photo (fridge, shelf, etc.).
|
||||
func (c *Client) RecognizeProducts(ctx context.Context, imageBase64, mimeType string) ([]RecognizedItem, error) {
|
||||
func (c *Client) RecognizeProducts(ctx context.Context, imageBase64, mimeType string) ([]ai.RecognizedItem, error) {
|
||||
prompt := `Ты — система распознавания продуктов питания.
|
||||
|
||||
Посмотри на фото и определи все видимые продукты питания.
|
||||
@@ -131,19 +79,19 @@ func (c *Client) RecognizeProducts(ctx context.Context, imageBase64, mimeType st
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Items []RecognizedItem `json:"items"`
|
||||
Items []ai.RecognizedItem `json:"items"`
|
||||
}
|
||||
if err := parseJSON(text, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse products result: %w", err)
|
||||
}
|
||||
if result.Items == nil {
|
||||
return []RecognizedItem{}, nil
|
||||
return []ai.RecognizedItem{}, nil
|
||||
}
|
||||
return result.Items, nil
|
||||
}
|
||||
|
||||
// RecognizeDish uses the vision model to identify a dish and estimate its nutritional content.
|
||||
func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string) (*DishResult, error) {
|
||||
func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string) (*ai.DishResult, error) {
|
||||
prompt := `Ты — диетолог и кулинарный эксперт.
|
||||
|
||||
Посмотри на фото блюда и определи:
|
||||
@@ -171,7 +119,7 @@ func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string
|
||||
return nil, fmt.Errorf("recognize dish: %w", err)
|
||||
}
|
||||
|
||||
var result DishResult
|
||||
var result ai.DishResult
|
||||
if err := parseJSON(text, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse dish result: %w", err)
|
||||
}
|
||||
@@ -183,7 +131,7 @@ func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string
|
||||
|
||||
// ClassifyIngredient uses the text model to classify an unknown food item
|
||||
// and build an ingredient_mappings record for it.
|
||||
func (c *Client) ClassifyIngredient(ctx context.Context, name string) (*IngredientClassification, error) {
|
||||
func (c *Client) ClassifyIngredient(ctx context.Context, name string) (*ai.IngredientClassification, error) {
|
||||
prompt := fmt.Sprintf(`Classify the food product: "%s".
|
||||
Return ONLY valid JSON without markdown:
|
||||
{
|
||||
@@ -209,7 +157,7 @@ Return ONLY valid JSON without markdown:
|
||||
return nil, fmt.Errorf("classify ingredient: %w", err)
|
||||
}
|
||||
|
||||
var result IngredientClassification
|
||||
var result ai.IngredientClassification
|
||||
if err := parseJSON(text, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse classification: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user