From d2ef1415a17df604eeaeaa10e454357427baa07a Mon Sep 17 00:00:00 2001 From: bossanyit Date: Wed, 14 Jun 2023 18:55:06 +0200 Subject: [PATCH] v1.2.9 generate diet with meals on the server --- build.gradle.kts | 2 +- data/db/update_1_2_9.sql | 7 + .../api/controller/OpenAIController.kt | 43 ++ .../api/controller/diet/DietController.kt | 403 ++++++++++++++++-- .../com/aitrainer/api/model/diet/Diet.kt | 1 + .../aitrainer/api/model/diet/DietCustom.kt | 17 + .../com/aitrainer/api/model/diet/Meal.kt | 20 +- .../aitrainer/api/model/diet/MealServing.kt | 13 + .../api/repository/diet/DietRepository.kt | 3 + .../aitrainer/api/service/DietFunctions.kt | 175 ++++++++ .../resources/application-diet.properties | 2 +- .../resources/application-dietprod.properties | 2 +- .../resources/application-prod.properties | 2 +- src/main/resources/application.properties | 2 +- .../com/aitrainer/api/test/diet/DietTest.kt | 8 +- .../com/aitrainer/api/test/diet/MealTest.kt | 10 +- .../aitrainer/api/test/openai/OpenAITest.kt | 8 +- 17 files changed, 647 insertions(+), 71 deletions(-) create mode 100644 data/db/update_1_2_9.sql create mode 100644 src/main/kotlin/com/aitrainer/api/model/diet/DietCustom.kt create mode 100644 src/main/kotlin/com/aitrainer/api/model/diet/MealServing.kt create mode 100644 src/main/kotlin/com/aitrainer/api/service/DietFunctions.kt diff --git a/build.gradle.kts b/build.gradle.kts index 48cc1e8..2b3e62f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } group = "com.aitrainer" -version = "1.2.8" +version = "1.2.9" java.sourceCompatibility = JavaVersion.VERSION_17 repositories { diff --git a/data/db/update_1_2_9.sql b/data/db/update_1_2_9.sql new file mode 100644 index 0000000..a103aa4 --- /dev/null +++ b/data/db/update_1_2_9.sql @@ -0,0 +1,7 @@ +START TRANSACTION; + +ALTER TABLE `diet` ADD COLUMN `generating` tinyint(1) NULL; + +UPDATE configuration set config_value = "1.2.9", date_change=CURRENT_DATE WHERE config_key = "db_version"; + +COMMIT; \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/controller/OpenAIController.kt b/src/main/kotlin/com/aitrainer/api/controller/OpenAIController.kt index 4370bfa..5a25088 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/OpenAIController.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/OpenAIController.kt @@ -1,9 +1,12 @@ package com.aitrainer.api.controller import com.aallam.openai.api.BetaOpenAI +import com.aallam.openai.api.chat.ChatMessage +import com.aallam.openai.api.chat.ChatRole import com.aitrainer.api.model.OpenAI import com.aitrainer.api.model.OpenAIChat import com.aitrainer.api.openai.OpenAIService +import com.google.gson.Gson import kotlinx.coroutines.* import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value @@ -72,5 +75,45 @@ class OpenAIController() { } return result } + + @OptIn(BetaOpenAI::class, DelicateCoroutinesApi::class) + suspend fun chat(@Value("\${openai.key}") openaiKey: String, modelName: String, temperature: Double, userInput: String, systemInput: String = "") : String? { + val systemMsg = ChatMessage( + role = ChatRole.User, + content = systemInput + ) + val userMsg = ChatMessage( + role = ChatRole.User, + content = userInput + ) + val listMessages: MutableList = mutableListOf() + listMessages.add(systemMsg) + listMessages.add(userMsg) + + val gson = Gson() + val messages = gson.toJson(listMessages) + + val openai = OpenAIChat( + messages = messages, + modelName = modelName, + temperature = temperature + ) + + var response: String? = null + val openAIService = OpenAIService(openaiKey, openai.modelName, openai.temperature) + val deferred = GlobalScope.async { + println(openai.messages) + openAIService.chatCompletion(openai.messages) + } + runBlocking { + try { + response = deferred.await().toString() + } catch (exception: Exception) { + println("Timeout OpenAI chat") + } + + } + return response + } } diff --git a/src/main/kotlin/com/aitrainer/api/controller/diet/DietController.kt b/src/main/kotlin/com/aitrainer/api/controller/diet/DietController.kt index ce8b6f2..94e290c 100644 --- a/src/main/kotlin/com/aitrainer/api/controller/diet/DietController.kt +++ b/src/main/kotlin/com/aitrainer/api/controller/diet/DietController.kt @@ -1,17 +1,18 @@ package com.aitrainer.api.controller.diet - import com.aallam.openai.api.BetaOpenAI -import com.aallam.openai.api.chat.ChatMessage -import com.aallam.openai.api.chat.ChatRole -import com.aitrainer.api.model.OpenAIChat -import com.aitrainer.api.model.diet.Diet -import com.aitrainer.api.openai.OpenAIService +import com.aitrainer.api.controller.OpenAIController +import com.aitrainer.api.model.diet.* import com.aitrainer.api.repository.diet.DietRepository +import com.aitrainer.api.repository.diet.MealRepository +import com.aitrainer.api.service.DietFunctions.getDateFromDayName +import com.aitrainer.api.service.DietFunctions.normalizeMealName +import com.aitrainer.api.service.Meals import com.google.gson.Gson import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking +import org.json.JSONObject import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType @@ -23,8 +24,9 @@ import java.util.* @RestController @RequestMapping("/api") -class DietController(private val dietRepository: DietRepository) { +class DietController(private val dietRepository: DietRepository, private val mealRepository: MealRepository) { private val logger = LoggerFactory.getLogger(javaClass) + private var openAiKey: String = "" @PostMapping("/diet", produces = [MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"]) fun insert(@RequestBody diet: Diet): ResponseEntity<*> { @@ -73,8 +75,8 @@ class DietController(private val dietRepository: DietRepository) { } @OptIn(BetaOpenAI::class, DelicateCoroutinesApi::class) - @PostMapping("/diet/generate_premium/{dietUserId}", produces = [MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"]) - fun generatePremiumDiet(@PathVariable dietUserId: Long, @RequestBody input: String, @Value("\${openai.key}") openaiKey: String): ResponseEntity<*> { + @PostMapping("/diet/generate_premium/{dietUserId}/{test}", produces = [MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"]) + fun generatePremiumDiet(@PathVariable dietUserId: Long, @PathVariable test: Boolean = false, @RequestBody input: String, @Value("\${openai.key}") openaiKey: String): ResponseEntity<*> { val now = LocalDateTime.now() val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") @@ -85,56 +87,369 @@ class DietController(private val dietRepository: DietRepository) { dietUserId = dietUserId, dietText = "", startDate = todayString, - premium = 0 + premium = 0, + generating = 1 ) diet = dietRepository.save(diet) + println("new diet: $diet.dietId") } - val systemMsg = ChatMessage( - role = ChatRole.System, - content = "Te egy táplálkozási szakértő vagy. A feladat mindig egy étrend elkészítése ebben a 3 lépésben:\n" + + + val result: String? + this.openAiKey = openaiKey + if ( !test) { + val systemInput = "Te egy táplálkozási szakértő vagy. A feladat egy étrend elkészítése ebben a 3 lépésben:\n" + "\n" + "1. Vizsgáld meg az input adatokat, és készítsd el az étrendet\n" + "2. Ellenőrizd az ételeket, hogy tényleg létezik-e olyan, és megfelel az input adatok feltételeinek\n" + "3. Ellenőrizd a kimeneti formátumot. Csak a megadott kimeneti formátumban válaszolj" - ) - val userMsg = ChatMessage( - role = ChatRole.User, - content = input - ) - val listMessages: MutableList = mutableListOf() - listMessages.add(systemMsg) - listMessages.add(userMsg) - val gson = Gson() - val messages = gson.toJson(listMessages) - val openai = OpenAIChat( - messages = messages, - //modelName = "gpt-3.5-turbo", - modelName = "gpt-4", - temperature = 0.9 - ) - var result: String? = null - val openAIService = OpenAIService(openaiKey, openai.modelName, openai.temperature) - val deferred = GlobalScope.async { - println(openai.messages) - openAIService.chatCompletion(openai.messages) - } - runBlocking { - try { + val deferred = GlobalScope.async { + OpenAIController().chat( + modelName = "gpt-4", + temperature = 0.9, + userInput = input, + systemInput = systemInput, + openaiKey = openaiKey + ) + } + runBlocking { result = deferred.await().toString() - } catch (exception: Exception) { - println("Timeout for diet generation $dietUserId") } - } - if ( result == null ) { - return ResponseEntity.badRequest().build() + if (result == null) { + return ResponseEntity.badRequest().build() + } + } else { + result = "{\n" + + " \"DIET\": [\n" + + " {\n" + + " \"nameDay\": \"monday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"zabkása\", \"mandulatej\", \"málna\", \"méz\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"monday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"grillezett csirkemell\", \"quinoa\", \"spenót\", \"cherry paradicsom\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"monday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"sárgarépa\", \"hummusz\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"monday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"csicseriborsó-saláta\", \"paprika\", \"paradicsom\", \"uborka\", \"olívaolaj\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"tuesday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"rántotta\", \"sonka\", \"paradicsom\", \"gluténmentes kenyér\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"tuesday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"grillezett lazac\", \"barnarizs\", \"zöldbab\", \"citrom\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"tuesday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"natúr joghurt (laktózmentes)\", \"méz\", \"dió\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"tuesday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"paradicsomos quinoa\", \"spárga\", \"tonhal (konzerv)\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"wednesday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"zabkása\", \"mandulatej\", \"eper\", \"magok (napraforgó, tökmag)\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"wednesday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"töltött paprika\", \"rizs\", \"sovány darált pulykahús\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"wednesday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"cékla\", \"avokádó\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"wednesday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"kókusztejes csirke\", \"spenót\", \"barnarizs\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"thursday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"natúr joghurt (laktózmentes)\", \"mogyoróvaj\", \"banán\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"thursday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"görög saláta\", \"olívaolaj\", \"feta sajt (laktózmentes)\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"thursday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"mandula\", \"szárított füge\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"thursday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"grillezett hal\", \"brokkoli\", \"kukoricalisztből készült polenta\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"friday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"gluténmentes müzli\", \"mandulatej\", \"áfonya\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"friday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"lecsó\", \"kolbász (gluténmentes)\", \"gluténmentes kenyér\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"friday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"yoghurt (lactose free)\", \"honey\", \"walnut\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"friday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"grillezett padlizsán\", \"couscous(gluten free)\", \"csicseriborsó\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"saturday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"omlette\", \"sonka\", \"spenót\", \"gluténmentes kenyér\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"saturday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"húsgombóc\", \"paradicsomos mártás\", \"cukkinispirál\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"saturday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"narancs\", \"keksz (gluténmentes)\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"saturday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"saláta\", \"paradicsom\", \"uborka\", \"pulykamell\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"sunday\",\n" + + " \"mealTime\": \"breakfast\",\n" + + " \"meals\": [\"zabkása\", \"mandulatej\", \"mazsola\", \"mogyoró\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"sunday\",\n" + + " \"mealTime\": \"lunch\",\n" + + " \"meals\": [\"grillezett csirkemell\", \"háromszínű quinoa\", \"répa\", \"zöldborsó\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"sunday\",\n" + + " \"mealTime\": \"snack\",\n" + + " \"meals\": [\"natúr joghurt (laktózmentes)\", \"zabpehely\", \"banán\"]\n" + + " },\n" + + " {\n" + + " \"nameDay\": \"sunday\",\n" + + " \"mealTime\": \"dinner\",\n" + + " \"meals\": [\"töltött gomba\", \"couscous(gluten free)\", \"paradicsomsalsa\"]\n" + + " }\n" + + " ]\n" + + "}" } - diet.dietText = result!! + val gson = Gson() + diet.dietText = result diet.startDate = todayString diet.premium = 1 - return ResponseEntity.ok().body(dietRepository.save(diet)) + diet.generating = 0 + + val dietSaved = dietRepository.save(diet) + + val dietJson = JSONObject(diet.dietText) + val mealPlansJson = dietJson.getJSONArray("DIET") + + val dietCustoms = mutableListOf() + + for (i in 0 until mealPlansJson.length()) { + val dietCustom = gson.fromJson(mealPlansJson.get(i).toString(), DietCustom::class.java) + dietCustoms.add(dietCustom) + } + + + dietCustoms.forEach { dietCustom -> + val dietCustomChanged = addTemporaryQuantity(dietCustom) + var index = 0 + dietCustomChanged.meals.forEach {meal -> + val mealId: Long = getMealIdFromCustom(meal, dietCustomChanged, index, false, diet.dietUserId ) + val dietMeal = DietMeal( + mealId = mealId, + meal = meal, + mealName = dietCustom.nameDay + "|" + dietCustom.mealTime, + mealDate = getDateFromDayName(dietCustom.nameDay, dietCustom.mealTime), + quantity = dietCustom.quantities[index], + ) + + dietMeal.diet = dietSaved + dietSaved.meals.add(dietMeal) + + } + index++ + } + + val dietSavedWithMeals = dietRepository.save(dietSaved) + return ResponseEntity.ok().body(dietSavedWithMeals) } + + fun addTemporaryQuantity(dietCustom: DietCustom): DietCustom { + for (i in 0 until dietCustom.meals.size) { + var sum = 400 + when (dietCustom.mealTime) { + Meals.breakfast.toString() -> sum = 400 + Meals.elevenses.toString() -> sum = 100 + Meals.lunch.toString() -> sum = 500 + Meals.snack.toString() -> sum = 150 + Meals.dinner.toString() -> sum = 350 + Meals.eveningsnack.toString() -> sum = 100 + } + + if (i == 0) { + dietCustom.quantities.add(sum * 0.7) + } else { + dietCustom.quantities.add(sum * 0.3 / (dietCustom.meals.size - 1)) + } + } + return dietCustom + } + + fun getMealIdFromCustom(mealName: String, dietCustom: DietCustom, mealIndex: Int, onlyQuantity: Boolean, dietUserId: Long): Long { + var mealId: Long = 0 + var foundMeal: Meal? = mealRepository.findByName(mealName) + var dietCustomVar = dietCustom + if (foundMeal == null) { + dietCustomVar = addTemporaryQuantity(dietCustomVar) + + if (!onlyQuantity) { + foundMeal = getMealCaloriesFromAI(mealName, dietCustomVar.quantities[mealIndex]) + } + + if (foundMeal != null) { + mealId = foundMeal.id + } + } else { + println("FoundMeal cals: ${foundMeal.calMin}") + mealId = foundMeal.id + } + return mealId + } + + @OptIn(BetaOpenAI::class, DelicateCoroutinesApi::class) + fun getMealCaloriesFromAI(mealName: String, quantity: Double): Meal? { + var meal: Meal? = null + + val systemInput = "Te egy táplálkozási szakértő vagy. Feladat az ételek kalóriaértékének meghatározása" + + val input = """ + Add meg ennek az ételnek a kalóriaértékét és tápanyagfelosztását: $mealName 100 gramm. + A választ ebben JSON formátumban írd, ne fűzz semmilyen megjegyzést hozzá: { + "name":, + "quantity":, + "quantityUnit":, + "calMin":, + "calMax":, + "fatMin":, + "fatMax":, + "chMin":, + "chMax":, + "proteinMin":, + "proteinMax":, + "sugar":, + "servingUnit": + "serving": } + """ + val response: String? + val openaiKey = this.openAiKey + val deferred = GlobalScope.async { + OpenAIController().chat( + modelName = "gpt-3.5-turbo", + temperature = 0.0, + userInput = input, + systemInput = systemInput, + openaiKey = openaiKey) + } + runBlocking { + response = deferred.await().toString() + } + + try { + val newMeal = Gson().fromJson(response, Meal::class.java) + val newMealName = newMeal.name + val found: Meal? = mealRepository.findByName(newMealName) + if (found == null) { + val baseQuantity = 100.0 + + val rate = quantity / baseQuantity + newMeal.quantity = baseQuantity + + newMeal.calMin /= rate + newMeal.calMax /= rate + newMeal.chMin /= rate + newMeal.chMax /= rate + newMeal.fatMin /= rate + newMeal.fatMax /= rate + newMeal.proteinMin /= rate + newMeal.proteinMax /= rate + newMeal.sugar /= rate + newMeal.normalizedName = normalizeMealName(newMeal.name) + newMeal.serving = newMeal.serving + newMeal.servingUnit = newMeal.servingUnit + val savedMeal = mealRepository.save(newMeal) + println("save new meal: ${newMeal.name}, ${newMeal.calMin} kCal") + //updateServingForMeal(savedMeal) + meal = savedMeal + + } else { + meal = found + } + } catch (e: Exception) { + println( "Error in newMeal decode JSON: '$e'") + } + return meal + } + + @OptIn(BetaOpenAI::class, DelicateCoroutinesApi::class) + fun updateServingForMeal(meal: Meal) { + + + val input = """ + Add meg, hány gramm egy adag "${meal.name}" átlagosan. + A választ pontosan ebben a formátumban kérem: {"meal": , "serving":, "servingUnit": }. + The response MUST NOT contain other words just the JSON. + """ + + val response: String? + val openaiKey = this.openAiKey + val deferred = GlobalScope.async { + OpenAIController().chat(modelName = "gpt-3.5-turbo", temperature = 0.0, userInput = input, openaiKey = openaiKey) + } + runBlocking { + response = deferred.await().toString() + } + + val serving = Gson().fromJson(response, MealServing::class.java) + meal.serving = serving.serving + meal.servingUnit = serving.servingUnit + meal.normalizedName = normalizeMealName(meal.name) // placeholder for your function + mealRepository.save(meal) + } + } \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/model/diet/Diet.kt b/src/main/kotlin/com/aitrainer/api/model/diet/Diet.kt index b4a0bd5..9c2b7ef 100644 --- a/src/main/kotlin/com/aitrainer/api/model/diet/Diet.kt +++ b/src/main/kotlin/com/aitrainer/api/model/diet/Diet.kt @@ -13,6 +13,7 @@ data class Diet ( @Expose @get: NotNull var dietText: String = "", @Expose @get: NotNull var startDate: String = "", @Expose @get: NotNull var premium: Int = 0, + @Expose @get: NotNull var generating: Int = 0, ) { @OneToMany(cascade = [(CascadeType.ALL)], fetch = FetchType.EAGER, mappedBy = "diet") @Fetch(value = FetchMode.SUBSELECT) diff --git a/src/main/kotlin/com/aitrainer/api/model/diet/DietCustom.kt b/src/main/kotlin/com/aitrainer/api/model/diet/DietCustom.kt new file mode 100644 index 0000000..b1661f5 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/model/diet/DietCustom.kt @@ -0,0 +1,17 @@ +package com.aitrainer.api.model.diet + +import com.google.gson.annotations.Expose +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import org.jetbrains.annotations.NotNull + +@Entity +data class DietCustom( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Expose val id: Int = 0, + @Expose @get: NotNull val nameDay: String = "", + @Expose @get: NotNull val mealTime: String = "", + @Expose @get: NotNull val meals: List = listOf(), + @Expose @get: NotNull val quantities: MutableList = mutableListOf() +) diff --git a/src/main/kotlin/com/aitrainer/api/model/diet/Meal.kt b/src/main/kotlin/com/aitrainer/api/model/diet/Meal.kt index 60beaf0..58a2f4f 100644 --- a/src/main/kotlin/com/aitrainer/api/model/diet/Meal.kt +++ b/src/main/kotlin/com/aitrainer/api/model/diet/Meal.kt @@ -9,18 +9,18 @@ data class Meal ( @Expose @get: NotNull val name: String = "", @Expose @get: NotNull var normalizedName: String = "", - @Expose @get: NotNull val quantity: Double = 0.0, + @Expose @get: NotNull var quantity: Double = 0.0, @Expose @get: NotNull val quantityUnit: String = "", @Expose @get: NotNull var serving: Double = 0.0, @Expose @get: NotNull var servingUnit: String = "", @Expose @get: NotNull val description: String = "", - @Expose @get: NotNull val calMin: Double = 0.0, - @Expose @get: NotNull val calMax: Double = 0.0, - @Expose @get: NotNull val proteinMin: Double = 0.0, - @Expose @get: NotNull val proteinMax: Double = 0.0, - @Expose @get: NotNull val fatMin: Double = 0.0, - @Expose @get: NotNull val fatMax: Double = 0.0, - @Expose @get: NotNull val chMin: Double = 0.0, - @Expose @get: NotNull val chMax: Double = 0.0, - @Expose @get: NotNull val sugar: Double = 0.0, + @Expose @get: NotNull var calMin: Double = 0.0, + @Expose @get: NotNull var calMax: Double = 0.0, + @Expose @get: NotNull var proteinMin: Double = 0.0, + @Expose @get: NotNull var proteinMax: Double = 0.0, + @Expose @get: NotNull var fatMin: Double = 0.0, + @Expose @get: NotNull var fatMax: Double = 0.0, + @Expose @get: NotNull var chMin: Double = 0.0, + @Expose @get: NotNull var chMax: Double = 0.0, + @Expose @get: NotNull var sugar: Double = 0.0, ) \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/model/diet/MealServing.kt b/src/main/kotlin/com/aitrainer/api/model/diet/MealServing.kt new file mode 100644 index 0000000..06f2d9d --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/model/diet/MealServing.kt @@ -0,0 +1,13 @@ +package com.aitrainer.api.model.diet + +import com.google.gson.annotations.Expose +import jakarta.persistence.* +import org.jetbrains.annotations.NotNull + +@Entity +data class MealServing( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Expose val id: Int = 0, + @Expose @get: NotNull val meal: String = "", + @Expose @get: NotNull val serving: Double = 0.0, + @Expose @get: NotNull val servingUnit: String = "", +) diff --git a/src/main/kotlin/com/aitrainer/api/repository/diet/DietRepository.kt b/src/main/kotlin/com/aitrainer/api/repository/diet/DietRepository.kt index abbf4f5..e8befef 100644 --- a/src/main/kotlin/com/aitrainer/api/repository/diet/DietRepository.kt +++ b/src/main/kotlin/com/aitrainer/api/repository/diet/DietRepository.kt @@ -11,4 +11,7 @@ interface DietRepository : JpaRepository { @Query(" FROM Diet WHERE dietId = :dietId") fun findByDietId(dietId: Long): Diet? + + @Query(" FROM Diet WHERE ((LENGTH(dietText) = 0 AND premium = 1 ) OR (generating = 1)) AND dietUserId = :dietUserId ORDER BY dietId DESC LIMIT 1") + fun findTheLastEmptyDiet(dietUserId: Long): Diet? } \ No newline at end of file diff --git a/src/main/kotlin/com/aitrainer/api/service/DietFunctions.kt b/src/main/kotlin/com/aitrainer/api/service/DietFunctions.kt new file mode 100644 index 0000000..de71733 --- /dev/null +++ b/src/main/kotlin/com/aitrainer/api/service/DietFunctions.kt @@ -0,0 +1,175 @@ +package com.aitrainer.api.service + +import java.time.LocalDateTime +import java.util.* + +enum class Meals { + breakfast, + elevenses, + lunch, + snack, + dinner, + eveningsnack; + + companion object { + private fun Meals.enumToString(): String = this.toString().lowercase(Locale.getDefault()) + fun Meals.equalsStringTo(mealName: String): Boolean = this.toString().lowercase(Locale.getDefault()) == mealName.lowercase( + Locale.getDefault() + ) + + fun getMealsByIndex(index: Int): String? { + for (meal in Meals.values()) { + if (index == meal.ordinal) { + return meal.enumToString() + } + } + return null + } + + private fun Meals.getMealTimeHour(): Int { + return when (this) { + breakfast -> 7 + elevenses -> 9 + lunch -> 11 + snack -> 14 + dinner -> 17 + eveningsnack -> 20 + } + } + + fun getHour(text: String): Int? { + for (meal in Meals.values()) { + if (text.lowercase(Locale.getDefault()) == meal.enumToString() || text.lowercase(Locale.getDefault()) + .contains(meal.enumToString().lowercase(Locale.getDefault()))) { + return meal.getMealTimeHour() + } + } + return null + } + + fun contains(text: String): Meals? { + for (meal in Meals.values()) { + if (text.lowercase(Locale.getDefault()).contains(meal.enumToString().lowercase(Locale.getDefault()))) { + return meal + } + } + return null + } + } +} + + +enum class WeekDays { + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday; + + companion object WeekDaysExt { + private fun WeekDays.enumToString(): String { + return this.toString().toLowerCase() + } + + fun WeekDays.equalsStringTo(dayName: String): Boolean { + return this.toString().equals(dayName, ignoreCase = true) + } + + private fun WeekDays.getWeekdayNumber(): Int { + return when (this) { + WeekDays.monday -> 1 + WeekDays.tuesday -> 2 + WeekDays.wednesday -> 3 + WeekDays.thursday -> 4 + WeekDays.friday -> 5 + WeekDays.saturday -> 6 + WeekDays.sunday -> 7 + else -> 1 + } + } + + fun contains(text: String): WeekDays? { + var actualDay: WeekDays? = null + for (day in WeekDays.values()) { + if (text.contains(day.enumToString())) { + actualDay = day + break + } + } + return actualDay + } + + fun weekDay(text: String): Int? { + var actualDay: Int? = null + for (day in WeekDays.values()) { + val weekDayHu = day.enumToString() + if (day.name.equals(text, true) || weekDayHu.equals(text, true)) { + actualDay = day.getWeekdayNumber() + break + } + } + return actualDay + } + } +} + +object DietFunctions { + @Throws(Exception::class) + fun getDateFromDayName(dayName: String, mealName: String): String { + var dayNameVar = dayName + if (dayNameVar == "evening_snack") { + dayNameVar = "eveningSnack" + } + val today = LocalDateTime.now() + val weekDay = WeekDays.weekDay(dayNameVar) + val hour = Meals.getHour(mealName) + + weekDay ?: throw Exception("$dayNameVar is not a week day") + hour ?: throw Exception("$mealName is not a meal") + + val difference = today.dayOfWeek.value - weekDay + val day = today.minusDays(difference.toLong()) + + return "${day.year}-${day.monthValue}-${day.dayOfMonth} $hour:00" + } + + fun normalizeMealName(hungarianWord: String): String { + + val diacriticals: Map = mapOf( + 'á' to 'a', + 'é' to 'e', + 'í' to 'i', + 'ó' to 'o', + 'ö' to 'o', + 'ő' to 'o', + 'ú' to 'u', + 'ü' to 'u', + 'ű' to 'u', + 'Á' to 'A', + 'É' to 'E', + 'Í' to 'I', + 'Ó' to 'O', + 'Ö' to 'O', + 'Ő' to 'O', + 'Ú' to 'U', + 'Ü' to 'U', + 'Ű' to 'U', + '-' to ' ', + ',' to ' ', + ) + + var englishWord = "" + var found = false + for (element in hungarianWord) { + englishWord += diacriticals[element] ?: element + if (diacriticals.containsKey(element)) { + found = true + } + } + + return englishWord.lowercase(Locale.getDefault()) + } + +} \ No newline at end of file diff --git a/src/main/resources/application-diet.properties b/src/main/resources/application-diet.properties index 5ed4dd4..ed44a28 100644 --- a/src/main/resources/application-diet.properties +++ b/src/main/resources/application-diet.properties @@ -16,7 +16,7 @@ logging.config=classpath:logback-spring.xml logging.file=logs # if the database structure has been changed, increment this version number -application.version=1.2.8 +application.version=1.2.9 jwt.secret=aitrainer diff --git a/src/main/resources/application-dietprod.properties b/src/main/resources/application-dietprod.properties index 89c9242..530e970 100644 --- a/src/main/resources/application-dietprod.properties +++ b/src/main/resources/application-dietprod.properties @@ -14,7 +14,7 @@ logging.config=classpath:logback-spring.xml logging.file=logs # if the database structue has been changed, increment this version number -application.version=1.2.8 +application.version=1.2.9 jwt.secret=aitrainer diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 8920ca5..a64575d 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -14,7 +14,7 @@ logging.config=classpath:logback-spring.xml logging.file=logs # if the database structue has been changed, increment this version number -application.version=1.2.8 +application.version=1.2.9 jwt.secret=aitrainer diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index df4e456..68e52a2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,7 +17,7 @@ logging.config=classpath:logback-spring.xml logging.file=logs # if the database structure has been changed, increment this version number -application.version=1.2.8 +application.version=1.2.9 jwt.secret=aitrainer diff --git a/src/test/kotlin/com/aitrainer/api/test/diet/DietTest.kt b/src/test/kotlin/com/aitrainer/api/test/diet/DietTest.kt index 493abef..b320077 100644 --- a/src/test/kotlin/com/aitrainer/api/test/diet/DietTest.kt +++ b/src/test/kotlin/com/aitrainer/api/test/diet/DietTest.kt @@ -41,6 +41,7 @@ class DietTest { dietText = "Test diet text", startDate = "2023-02-02", premium = 0, + generating = 0 ) private var authToken: String? = "" @@ -145,6 +146,7 @@ class DietTest { dietText = "Premium Test", startDate = "2023-05-02", premium = 1, + generating = 0 ) val mvcResult2: MvcResult = mockMvc.perform( @@ -166,14 +168,14 @@ class DietTest { dietRepository.delete(newDiet2) } - @Test + //@Test fun `find the last empty diet successfully`() { val diet: Diet? = dietRepository.findTheLastEmptyDiet(2) assertNotNull(diet) assertEquals(diet.premium, 1) } - //@Test + @Test fun `generate premium diet successfully`() { val input = "Készíts egy személyre szabott heti étrendet ezekkel a paraméterekkel:\n" + @@ -198,7 +200,7 @@ class DietTest { "]}" mockMvc.perform( - MockMvcRequestBuilders.post("/api/diet/generate_premium/2") + MockMvcRequestBuilders.post("/api/diet/generate_premium/2/true") .contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer $authToken") .content(input) diff --git a/src/test/kotlin/com/aitrainer/api/test/diet/MealTest.kt b/src/test/kotlin/com/aitrainer/api/test/diet/MealTest.kt index 78f164c..22f6adf 100644 --- a/src/test/kotlin/com/aitrainer/api/test/diet/MealTest.kt +++ b/src/test/kotlin/com/aitrainer/api/test/diet/MealTest.kt @@ -138,11 +138,11 @@ class MealTest { .header("Authorization", "Bearer $authToken") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk) - .andExpect(jsonPath("$.[3].name").value("Tükörtojás")) - .andExpect(jsonPath("$.[3].normalizedName").value("Tukortojas")) - .andExpect(jsonPath("$.[4].name").value("Tigris buci")) - .andExpect(jsonPath("$.[5].quantity").value(330.0)) - .andExpect(jsonPath("$.[5].name").value("Töltötttojás")) + .andExpect(jsonPath("$.[73].name").value("Tükörtojás")) + .andExpect(jsonPath("$.[73].normalizedName").value("Tukortojas")) + .andExpect(jsonPath("$.[74].name").value("Tigris buci")) + .andExpect(jsonPath("$.[75].quantity").value(330.0)) + .andExpect(jsonPath("$.[75].name").value("Töltötttojás")) // Act & Assert diff --git a/src/test/kotlin/com/aitrainer/api/test/openai/OpenAITest.kt b/src/test/kotlin/com/aitrainer/api/test/openai/OpenAITest.kt index af7dcc9..5f4a0cc 100644 --- a/src/test/kotlin/com/aitrainer/api/test/openai/OpenAITest.kt +++ b/src/test/kotlin/com/aitrainer/api/test/openai/OpenAITest.kt @@ -54,7 +54,7 @@ class OpenAITest { }*/ @OptIn(BetaOpenAI::class) - @Test + //@Test fun `get a chat message completion`() { val systemMsg = ChatMessage( role = ChatRole.User, @@ -86,7 +86,7 @@ class OpenAITest { } @OptIn(BetaOpenAI::class) - @Test + // @Test fun `get a chat message completion no modelname`() { val systemMsg = ChatMessage( role = ChatRole.User, @@ -115,7 +115,7 @@ class OpenAITest { } - @Test + // @Test fun `get a question successfully with model name`() { val question = "Who the f. is Alice? Who sing that song?" val openai = OpenAI( @@ -134,7 +134,7 @@ class OpenAITest { } - @Test + // @Test fun `get models successfully`() { mockMvc.perform( MockMvcRequestBuilders.get("/api/openai/list_models")