1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"repo": "alan-na/recipe-wiz", "files": {"backend/src/main/java/data_access/DeepSeekAiGateway.java": {"84f65e7590b84c9894f69a320b6ae7f8a09845d0": {"t": 1785745698.1793623, "n": 7084, "h": "1c58a3fe2b9d3db6", "c": "package data_access;\n\nimport entity.Nutrition;\nimport entity.Recipe;\nimport java.io.IOException;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport jakarta.annotation.PostConstruct;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\nimport use_case.ai_meal_plan.AiGenerationGateway;\n\n/**\n * Adapter that calls the DeepSeek Chat Completions API to generate structured meal plans.\n * Uses OkHttp (same pattern as NutritionAnalysisDataAccessObject) and org.json for parsing.\n */\n@Component\npublic class DeepSeekAiGateway implements AiGenerationGateway {\n\n private static final String DEEPSEEK_URL = \"https://api.deepseek.com/chat/completions\";\n private static final String MODEL = \"deepseek-chat\";\n private static final MediaType JSON_MEDIA = MediaType.get(\"application/json; charset=utf-8\");\n\n private static final String SYSTEM_PROMPT = \"\"\"\n You are a professional nutritionist and meal planner.\n You will receive a user's physical profile, their health goal, and a list of available recipes with nutritional data.\n Your task is to create a structured meal plan covering the specified date range.\n\n Rules:\n - Use ONLY recipeIds from the provided recipe list.\n - mealType MUST be exactly one of: Breakfast, Lunch, Dinner, Snack (case-sensitive).\n - mealDate MUST be in ISO-8601 format (YYYY-MM-DD) and within the requested date range (inclusive).\n - Assign 2-3 meals per day (e.g. Breakfast + Lunch + Dinner, optionally Snack).\n - Do NOT assign the same recipe more than once per day.\n - Distribute meals across the week to ensure nutritional variety.\n - Balance macronutrients according to the user's health goal.\n - Return ONLY a valid JSON object with this exact structure:\n { \"mealPlan\": [ { \"recipeId\": <integer>, \"mealDate\": \"<YYYY-MM-DD>\", \"mealType\": \"<string>\" } ] }\n - Return no prose, no markdown fences, only the JSON object.\n \"\"\";\n\n private final OkHttpClient httpClient;\n private final String apiKey;\n\n public DeepSeekAiGateway(\n OkHttpClient httpClient,\n @Value(\"${recipewiz.deepseek.api-key}\") String apiKey) {\n this.httpClient = httpClient;\n this.apiKey = apiKey;\n }\n\n @PostConstruct\n public void validateApiKey() {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\n \"DeepSeek API key is not configured. \" +\n \"Set the DEEPSEEK_API_KEY environment variable before starting the application.\");\n }\n }\n\n @Override\n public List<RawEntry> generatePlan(UserProfile profile, List<Recipe> recipes,\n LocalDate dateFrom, LocalDate dateTo) throws Exception {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is not configured.\");\n }\n\n String userMessage = buildUserMessage(profile, recipes, dateFrom, dateTo);\n String requestBodyJson = buildRequestBody(userMessage);\n\n Request request = new Request.Builder()\n .url(DEEPSEEK_URL)\n .addHeader(\"Authorization\", \"Bearer \" + apiKey)\n .post(RequestBody.create(requestBodyJson, JSON_MEDIA))\n .build();\n\n try (Response response = httpClient.newCall(request).execute()) {\n if (!response.isSuccessful() || response.body() == null) {\n throw new IOException(\"DeepSeek API returned HTTP \" + response.code());\n }\n return parseResponse(response.body().string());\n }\n }\n\n // ── Request builder ───────────────────────────────────────────────────────\n\n private String buildRequestBody(String userMessage) {\n JSONObject systemMsg = new JSONObject()\n .put(\"role\", \"system\")\n .put(\"content\", SYSTEM_PROMPT);\n\n JSONObject userMsg = new JSONObject()\n .put(\"role\", \"user\")\n .put(\"content\", userMessage);\n\n return new JSONObject()\n .put(\"model\", MODEL)\n .put(\"response_format\", new JSONObject().put(\"type\", \"json_object\"))\n .put(\"temperature\", 0.7)\n .put(\"max_tokens\", 2000)\n .put(\"messages\", new JSONArray().put(systemMsg).put(userMsg))\n .toString();\n }\n\n private String buildUserMessage(UserProfile p, List<Recipe> recipes,\n LocalDate from, LocalDate to) {\n StringBuilder sb = new StringBuilder();\n sb.append(\"=== User Profile ===\\n\");\n sb.append(\"Gender: \").append(p.gender()).append(\"\\n\");\n sb.append(\"Height: \").append(p.heightCm()).append(\" cm\\n\");\n sb.append(\"Weight: \").append(p.weightKg()).append(\" kg\\n\");\n sb.append(\"Age: \").append(p.age()).append(\" years\\n\");\n sb.append(\"Health Goal: \").append(p.goal()).append(\"\\n\\n\");\n\n sb.append(\"=== Meal Plan Date Range ===\\n\");\n sb.append(\"From: \").append(from).append(\" (inclusive)\\n\");\n sb.append(\"To: \").append(to).append(\" (inclusive)\\n\\n\");\n\n sb.append(\"=== Available Recipes (use ONLY these recipeIds) ===\\n\");\n for (Recipe r : recipes) {\n sb.append(\"recipeId=\").append(r.getRecipeId())\n .append(\" | title=\\\"\").append(r.getTitle()).append(\"\\\"\");\n Nutrition n = r.getNutrition();\n if (n != null) {\n sb.append(\" | cal=\").append(Math.round(n.getCalories()))\n .append(\"kcal, protein=\").append(Math.round(n.getProtein()))\n .append(\"g, fat=\").append(Math.round(n.getFat()))\n .append(\"g, carbs=\").append(Math.round(n.getCarbohydrates())).append(\"g\");\n }\n sb.append(\"\\n\");\n }\n return sb.toString();\n }\n\n // ── Response parser ───────────────────────────────────────────────────────\n\n private List<RawEntry> parseResponse(String responseBody) throws Exception {\n JSONObject root = new JSONObject(responseBody);\n String content = root\n .getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\")\n .getString(\"content\");\n\n JSONArray mealPlan = new JSONObject(content).getJSONArray(\"mealPlan\");\n List<RawEntry> result = new ArrayList<>();\n for (int i = 0; i < mealPlan.length(); i++) {\n JSONObject entry = mealPlan.getJSONObject(i);\n result.add(new RawEntry(\n entry.getInt(\"recipeId\"),\n LocalDate.parse(entry.getString(\"mealDate\")),\n entry.getString(\"mealType\")));\n }\n return result;\n }\n}\n"}, "f8f4f5f7b6ccdf17013cd655fc9925efaac72a97": {"t": 1785755751.7915378, "n": 6721, "h": "145e88f91fa4f6ee", "c": "package data_access;\n\nimport entity.Nutrition;\nimport entity.Recipe;\nimport java.io.IOException;\nimport java.time.LocalDate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\nimport use_case.ai_meal_plan.AiGenerationGateway;\n\n/**\n * Adapter that calls the DeepSeek Chat Completions API to generate structured meal plans.\n * Uses OkHttp (same pattern as NutritionAnalysisDataAccessObject) and org.json for parsing.\n */\n@Component\npublic class DeepSeekAiGateway implements AiGenerationGateway {\n\n private static final String DEEPSEEK_URL = \"https://api.deepseek.com/chat/completions\";\n private static final String MODEL = \"deepseek-chat\";\n private static final MediaType JSON_MEDIA = MediaType.get(\"application/json; charset=utf-8\");\n\n private static final String SYSTEM_PROMPT = \"\"\"\n You are a professional nutritionist and meal planner.\n You will receive a user's physical profile, their health goal, and a list of available recipes with nutritional data.\n Your task is to create a structured meal plan covering the specified date range.\n\n Rules:\n - Use ONLY recipeIds from the provided recipe list.\n - mealType MUST be exactly one of: Breakfast, Lunch, Dinner, Snack (case-sensitive).\n - mealDate MUST be in ISO-8601 format (YYYY-MM-DD) and within the requested date range (inclusive).\n - Assign 2-3 meals per day (e.g. Breakfast + Lunch + Dinner, optionally Snack).\n - Do NOT assign the same recipe more than once per day.\n - Distribute meals across the week to ensure nutritional variety.\n - Balance macronutrients according to the user's health goal.\n - Return ONLY a valid JSON object with this exact structure:\n { \"mealPlan\": [ { \"recipeId\": <integer>, \"mealDate\": \"<YYYY-MM-DD>\", \"mealType\": \"<string>\" } ] }\n - Return no prose, no markdown fences, only the JSON object.\n \"\"\";\n\n private final OkHttpClient httpClient;\n private final String apiKey;\n\n public DeepSeekAiGateway(\n OkHttpClient httpClient,\n @Value(\"${recipewiz.deepseek.api-key}\") String apiKey) {\n this.httpClient = httpClient;\n this.apiKey = apiKey;\n }\n\n @Override\n public List<RawEntry> generatePlan(UserProfile profile, List<Recipe> recipes,\n LocalDate dateFrom, LocalDate dateTo) throws Exception {\n if (apiKey == null || apiKey.isBlank()) {\n throw new IllegalStateException(\"DeepSeek API key is not configured.\");\n }\n\n String userMessage = buildUserMessage(profile, recipes, dateFrom, dateTo);\n String requestBodyJson = buildRequestBody(userMessage);\n\n Request request = new Request.Builder()\n .url(DEEPSEEK_URL)\n .addHeader(\"Authorization\", \"Bearer \" + apiKey)\n .post(RequestBody.create(requestBodyJson, JSON_MEDIA))\n .build();\n\n try (Response response = httpClient.newCall(request).execute()) {\n if (!response.isSuccessful() || response.body() == null) {\n throw new IOException(\"DeepSeek API returned HTTP \" + response.code());\n }\n return parseResponse(response.body().string());\n }\n }\n\n // ── Request builder ───────────────────────────────────────────────────────\n\n private String buildRequestBody(String userMessage) {\n JSONObject systemMsg = new JSONObject()\n .put(\"role\", \"system\")\n .put(\"content\", SYSTEM_PROMPT);\n\n JSONObject userMsg = new JSONObject()\n .put(\"role\", \"user\")\n .put(\"content\", userMessage);\n\n return new JSONObject()\n .put(\"model\", MODEL)\n .put(\"response_format\", new JSONObject().put(\"type\", \"json_object\"))\n .put(\"temperature\", 0.7)\n .put(\"max_tokens\", 2000)\n .put(\"messages\", new JSONArray().put(systemMsg).put(userMsg))\n .toString();\n }\n\n private String buildUserMessage(UserProfile p, List<Recipe> recipes,\n LocalDate from, LocalDate to) {\n StringBuilder sb = new StringBuilder();\n sb.append(\"=== User Profile ===\\n\");\n sb.append(\"Gender: \").append(p.gender()).append(\"\\n\");\n sb.append(\"Height: \").append(p.heightCm()).append(\" cm\\n\");\n sb.append(\"Weight: \").append(p.weightKg()).append(\" kg\\n\");\n sb.append(\"Age: \").append(p.age()).append(\" years\\n\");\n sb.append(\"Health Goal: \").append(p.goal()).append(\"\\n\\n\");\n\n sb.append(\"=== Meal Plan Date Range ===\\n\");\n sb.append(\"From: \").append(from).append(\" (inclusive)\\n\");\n sb.append(\"To: \").append(to).append(\" (inclusive)\\n\\n\");\n\n sb.append(\"=== Available Recipes (use ONLY these recipeIds) ===\\n\");\n for (Recipe r : recipes) {\n sb.append(\"recipeId=\").append(r.getRecipeId())\n .append(\" | title=\\\"\").append(r.getTitle()).append(\"\\\"\");\n Nutrition n = r.getNutrition();\n if (n != null) {\n sb.append(\" | cal=\").append(Math.round(n.getCalories()))\n .append(\"kcal, protein=\").append(Math.round(n.getProtein()))\n .append(\"g, fat=\").append(Math.round(n.getFat()))\n .append(\"g, carbs=\").append(Math.round(n.getCarbohydrates())).append(\"g\");\n }\n sb.append(\"\\n\");\n }\n return sb.toString();\n }\n\n // ── Response parser ───────────────────────────────────────────────────────\n\n private List<RawEntry> parseResponse(String responseBody) throws Exception {\n JSONObject root = new JSONObject(responseBody);\n String content = root\n .getJSONArray(\"choices\")\n .getJSONObject(0)\n .getJSONObject(\"message\")\n .getString(\"content\");\n\n JSONArray mealPlan = new JSONObject(content).getJSONArray(\"mealPlan\");\n List<RawEntry> result = new ArrayList<>();\n for (int i = 0; i < mealPlan.length(); i++) {\n JSONObject entry = mealPlan.getJSONObject(i);\n result.add(new RawEntry(\n entry.getInt(\"recipeId\"),\n LocalDate.parse(entry.getString(\"mealDate\")),\n entry.getString(\"mealType\")));\n }\n return result;\n }\n}\n"}}}} |