Files
food-ai/backend/internal/adapters/openai/recipe.go
dbastrikin cba50489be fix: generate recipes in the user's language, not always English
buildRecipePrompt ignored req.Lang entirely and hardcoded two
English instructions in the prompt. Added targetLang resolution
using the existing langNames map (from recognition.go) and
threaded it into both "Generate N recipes in …" and "All text
fields … MUST be in …" instructions. The image_query field
remains English since it is passed to the Pexels photo API.

Fixes recommendations and menu recipes being returned in English
regardless of the Accept-Language header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:42:57 +02:00

145 lines
3.8 KiB
Go

package openai
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/food-ai/backend/internal/adapters/ai"
)
// goalNames maps internal goal codes to English descriptions used in the prompt.
var goalNames = map[string]string{
"lose": "weight loss",
"maintain": "weight maintenance",
"gain": "muscle gain",
}
// GenerateRecipes generates recipes using the OpenAI API.
// Retries up to maxRetries times only when the response is not valid JSON.
// API-level errors (rate limits, auth, etc.) are returned immediately.
func (c *Client) GenerateRecipes(ctx context.Context, req ai.RecipeRequest) ([]ai.Recipe, error) {
prompt := buildRecipePrompt(req)
messages := []map[string]string{
{"role": "user", "content": prompt},
}
var lastErr error
for attempt := range maxRetries {
if attempt > 0 {
messages = []map[string]string{
{"role": "user", "content": prompt},
{"role": "user", "content": "Previous response was not valid JSON. Return ONLY a JSON array with no text before or after."},
}
}
text, err := c.generateContent(ctx, messages)
if err != nil {
return nil, err
}
recipes, err := parseRecipesJSON(text)
if err != nil {
lastErr = fmt.Errorf("attempt %d parse JSON: %w", attempt+1, err)
continue
}
for i := range recipes {
recipes[i].Nutrition.Approximate = true
}
return recipes, nil
}
return nil, fmt.Errorf("failed to parse valid JSON after %d attempts: %w", maxRetries, lastErr)
}
func buildRecipePrompt(req ai.RecipeRequest) string {
goal := goalNames[req.UserGoal]
if goal == "" {
goal = "weight maintenance"
}
targetLang := langNames[req.Lang]
if targetLang == "" {
targetLang = "English"
}
restrictions := "none"
if len(req.Restrictions) > 0 {
restrictions = strings.Join(req.Restrictions, ", ")
}
cuisines := "any"
if len(req.CuisinePrefs) > 0 {
cuisines = strings.Join(req.CuisinePrefs, ", ")
}
count := req.Count
if count <= 0 {
count = 5
}
perMealCalories := req.DailyCalories / 3
if perMealCalories <= 0 {
perMealCalories = 600
}
productsSection := ""
if len(req.AvailableProducts) > 0 {
productsSection = "\nAvailable products (⚠ = expiring soon, prioritise these):\n" +
strings.Join(req.AvailableProducts, "\n") + "\n"
}
return fmt.Sprintf(`You are a chef and nutritionist. Generate %d recipes in %s.
User profile:
- Goal: %s
- Daily calories: %d kcal
- Dietary restrictions: %s
- Cuisine preferences: %s
%s
Requirements for each recipe:
- Max %d kcal per serving
- Total cooking time: max 60 minutes
- Include approximate macros per serving
IMPORTANT:
- All text fields (title, description, ingredient names, units, step descriptions, tags) MUST be in %s.
- The "image_query" field MUST always be in English (it is used for stock-photo search).
Return ONLY a valid JSON array without markdown or extra text:
[{
"title": "...",
"description": "2-3 sentences",
"cuisine": "russian|asian|european|mediterranean|american|other",
"difficulty": "easy|medium|hard",
"prep_time_min": 10,
"cook_time_min": 20,
"servings": 2,
"image_query": "short English photo-search query",
"ingredients": [{"name": "...", "amount": 300, "unit": "..."}],
"steps": [{"number": 1, "description": "...", "timer_seconds": null}],
"tags": ["..."],
"nutrition_per_serving": {"calories": 420, "protein_g": 48, "fat_g": 12, "carbs_g": 18}
}]`, count, targetLang, goal, req.DailyCalories, restrictions, cuisines, productsSection, perMealCalories, targetLang)
}
func parseRecipesJSON(text string) ([]ai.Recipe, error) {
text = strings.TrimSpace(text)
if strings.HasPrefix(text, "```") {
text = strings.TrimPrefix(text, "```json")
text = strings.TrimPrefix(text, "```")
text = strings.TrimSuffix(text, "```")
text = strings.TrimSpace(text)
}
var recipes []ai.Recipe
if err := json.Unmarshal([]byte(text), &recipes); err != nil {
return nil, err
}
return recipes, nil
}