From 87275f61fc22083b65e2f4891634ac511d9b8c41 Mon Sep 17 00:00:00 2001 From: Sejin Park <95167215+sejineer@users.noreply.github.com> Date: Sat, 1 Jun 2024 12:21:19 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]:=20AI=20=EC=84=9C=EB=B2=84=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 알약 예측 API 임시 구현 * feat: 알약 예측 API 연동 * feat: ai 서버 연동 --- build.gradle.kts | 1 - .../client/example/AiServiceClient.kt | 13 ------ .../org/mediscan/client/example/ExampleApi.kt | 17 ------- .../mediscan/client/example/ExampleClient.kt | 14 ------ .../client/example/ExampleRequestDto.kt | 5 --- .../client/example/ExampleResponseDto.kt | 11 ----- .../example/model/ExampleClientResult.kt | 5 --- .../model/PillIdentificationRequestDto.kt | 11 ----- .../model/PillIdentificationResponseDto.kt | 6 --- .../src/main/resources/client-example.yml | 35 --------------- .../build.gradle.kts | 0 .../org/mediscan/client/AiServiceApi.kt | 22 +++++++++ .../org/mediscan/client/AiServiceClient.kt | 18 ++++++++ .../mediscan/client/AiServiceRequestDto.kt | 9 ++++ .../mediscan/client/AiServiceResponseDto.kt | 7 +++ .../org/mediscan/client/FeignConfig.kt} | 4 +- .../client/model/AiServiceClientResult.kt | 7 +++ core/core-api/build.gradle.kts | 2 +- .../response/PillIdentificationResponseDto.kt | 1 + .../api/domain/PillIdentificationService.kt | 29 ------------ .../mediscan/core/api/domain/PillManager.kt | 22 ++++----- .../mediscan/core/api/domain/PillService.kt | 45 +++++++++++++------ .../api/support/utils/FileMultipartFile.kt | 44 ++++++++++++++++++ settings.gradle.kts | 2 +- 24 files changed, 155 insertions(+), 175 deletions(-) delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/AiServiceClient.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleApi.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleClient.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleRequestDto.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleResponseDto.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/model/ExampleClientResult.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationRequestDto.kt delete mode 100644 clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationResponseDto.kt delete mode 100644 clients/client-example/src/main/resources/client-example.yml rename clients/{client-example => client}/build.gradle.kts (100%) create mode 100644 clients/client/src/main/kotlin/org/mediscan/client/AiServiceApi.kt create mode 100644 clients/client/src/main/kotlin/org/mediscan/client/AiServiceClient.kt create mode 100644 clients/client/src/main/kotlin/org/mediscan/client/AiServiceRequestDto.kt create mode 100644 clients/client/src/main/kotlin/org/mediscan/client/AiServiceResponseDto.kt rename clients/{client-example/src/main/kotlin/org/mediscan/client/example/ExampleConfig.kt => client/src/main/kotlin/org/mediscan/client/FeignConfig.kt} (70%) create mode 100644 clients/client/src/main/kotlin/org/mediscan/client/model/AiServiceClientResult.kt delete mode 100644 core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillIdentificationService.kt create mode 100644 core/core-api/src/main/kotlin/org/mediscan/core/api/support/utils/FileMultipartFile.kt diff --git a/build.gradle.kts b/build.gradle.kts index e1736df..36d91fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.springframework.boot.gradle.tasks.bundling.BootJar plugins { kotlin("jvm") diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/AiServiceClient.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/AiServiceClient.kt deleted file mode 100644 index 991190c..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/AiServiceClient.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.mediscan.client.example - -import org.mediscan.client.example.model.PillIdentificationRequestDto -import org.mediscan.client.example.model.PillIdentificationResponseDto -import org.springframework.cloud.openfeign.FeignClient -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody - -@FeignClient(name = "ai-service", url = "") -interface AiServiceClient { - @PostMapping("/identify-pill") - fun identifyPill(@RequestBody requestDto: PillIdentificationRequestDto): List -} diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleApi.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleApi.kt deleted file mode 100644 index 0e7b35c..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleApi.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.mediscan.client.example - -import org.springframework.cloud.openfeign.FeignClient -import org.springframework.http.MediaType -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestMethod - -@FeignClient(value = "example-api", url = "\${example.api.url}") -internal interface ExampleApi { - @RequestMapping( - method = [RequestMethod.POST], - value = ["/example/example-api"], - consumes = [MediaType.APPLICATION_JSON_VALUE], - ) - fun example(@RequestBody request: ExampleRequestDto): ExampleResponseDto -} diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleClient.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleClient.kt deleted file mode 100644 index 942cf8a..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleClient.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.mediscan.client.example - -import org.mediscan.client.example.model.ExampleClientResult -import org.springframework.stereotype.Component - -@Component -class ExampleClient internal constructor( - private val exampleApi: ExampleApi, -) { - fun example(exampleParameter: String): ExampleClientResult { - val request = ExampleRequestDto(exampleParameter) - return exampleApi.example(request).toResult() - } -} diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleRequestDto.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleRequestDto.kt deleted file mode 100644 index 66640ca..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleRequestDto.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.mediscan.client.example - -internal data class ExampleRequestDto( - val exampleRequestValue: String, -) diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleResponseDto.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleResponseDto.kt deleted file mode 100644 index eceff84..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleResponseDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.mediscan.client.example - -import org.mediscan.client.example.model.ExampleClientResult - -internal data class ExampleResponseDto( - val exampleResponseValue: String, -) { - fun toResult(): ExampleClientResult { - return ExampleClientResult(exampleResponseValue) - } -} diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/ExampleClientResult.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/ExampleClientResult.kt deleted file mode 100644 index fc6cc45..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/ExampleClientResult.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.mediscan.client.example.model - -data class ExampleClientResult( - val exampleResult: String, -) diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationRequestDto.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationRequestDto.kt deleted file mode 100644 index 766bd5b..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationRequestDto.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.mediscan.client.example.model - -import org.springframework.web.multipart.MultipartFile - -data class PillIdentificationRequestDto( - val frontImage: MultipartFile, - val backImage: MultipartFile, - val pillShape: String, - val frontMarking: String = "None", - val backMarking: String = "None", -) diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationResponseDto.kt b/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationResponseDto.kt deleted file mode 100644 index 79cc6c8..0000000 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/model/PillIdentificationResponseDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.mediscan.client.example.model - -data class PillIdentificationResponseDto( - var drugCode: String, - var confidence: Long, -) diff --git a/clients/client-example/src/main/resources/client-example.yml b/clients/client-example/src/main/resources/client-example.yml deleted file mode 100644 index 169ee0e..0000000 --- a/clients/client-example/src/main/resources/client-example.yml +++ /dev/null @@ -1,35 +0,0 @@ -example: - api: - url: https://default.example.example - -spring.cloud.openfeign: - client: - config: - example-api: - connectTimeout: 2100 - readTimeout: 5000 - loggerLevel: full - compression: - response: - enabled: false - httpclient: - max-connections: 2000 - max-connections-per-route: 500 - ---- -spring.config.activate.on-profile: local - ---- -spring.config.activate.on-profile: - - local-dev - - dev - ---- -spring.config.activate.on-profile: - - staging - - live - -example: - api: - url: https://live.example.example - diff --git a/clients/client-example/build.gradle.kts b/clients/client/build.gradle.kts similarity index 100% rename from clients/client-example/build.gradle.kts rename to clients/client/build.gradle.kts diff --git a/clients/client/src/main/kotlin/org/mediscan/client/AiServiceApi.kt b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceApi.kt new file mode 100644 index 0000000..431d76e --- /dev/null +++ b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceApi.kt @@ -0,0 +1,22 @@ +package org.mediscan.client + +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.multipart.MultipartFile + +@FeignClient(name = "ai-service", url = "127.0.0.1:8000") +internal interface AiServiceApi { + @RequestMapping( + method = [RequestMethod.POST], + value = ["/predict"], + consumes = [MediaType.MULTIPART_FORM_DATA_VALUE], + ) + fun predictPill( + @RequestPart frontImage: MultipartFile, + @RequestPart backImage: MultipartFile, + @RequestPart pillCsv: MultipartFile, + ): AiServiceResponseDto +} diff --git a/clients/client/src/main/kotlin/org/mediscan/client/AiServiceClient.kt b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceClient.kt new file mode 100644 index 0000000..59261a4 --- /dev/null +++ b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceClient.kt @@ -0,0 +1,18 @@ +package org.mediscan.client + +import org.mediscan.client.model.AiServiceClientResult +import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile + +@Component +class AiServiceClient internal constructor( + private val aiServiceApi: AiServiceApi, +) { + fun predict( + pillCsv: MultipartFile, + frontImage: MultipartFile, + backImage: MultipartFile, + ): List { + return aiServiceApi.predictPill(frontImage, backImage, pillCsv).result + } +} diff --git a/clients/client/src/main/kotlin/org/mediscan/client/AiServiceRequestDto.kt b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceRequestDto.kt new file mode 100644 index 0000000..e478e5a --- /dev/null +++ b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceRequestDto.kt @@ -0,0 +1,9 @@ +package org.mediscan.client + +import org.springframework.web.multipart.MultipartFile + +internal data class AiServiceRequestDto( + val frontImage: MultipartFile, + val backImage: MultipartFile, + val pillCsv: MultipartFile, +) diff --git a/clients/client/src/main/kotlin/org/mediscan/client/AiServiceResponseDto.kt b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceResponseDto.kt new file mode 100644 index 0000000..192df8a --- /dev/null +++ b/clients/client/src/main/kotlin/org/mediscan/client/AiServiceResponseDto.kt @@ -0,0 +1,7 @@ +package org.mediscan.client + +import org.mediscan.client.model.AiServiceClientResult + +internal data class AiServiceResponseDto( + val result: List, +) diff --git a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleConfig.kt b/clients/client/src/main/kotlin/org/mediscan/client/FeignConfig.kt similarity index 70% rename from clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleConfig.kt rename to clients/client/src/main/kotlin/org/mediscan/client/FeignConfig.kt index 506872d..494a784 100644 --- a/clients/client-example/src/main/kotlin/org/mediscan/client/example/ExampleConfig.kt +++ b/clients/client/src/main/kotlin/org/mediscan/client/FeignConfig.kt @@ -1,8 +1,8 @@ -package org.mediscan.client.example +package org.mediscan.client import org.springframework.cloud.openfeign.EnableFeignClients import org.springframework.context.annotation.Configuration @EnableFeignClients @Configuration -internal class ExampleConfig +internal class FeignConfig diff --git a/clients/client/src/main/kotlin/org/mediscan/client/model/AiServiceClientResult.kt b/clients/client/src/main/kotlin/org/mediscan/client/model/AiServiceClientResult.kt new file mode 100644 index 0000000..7e2718a --- /dev/null +++ b/clients/client/src/main/kotlin/org/mediscan/client/model/AiServiceClientResult.kt @@ -0,0 +1,7 @@ +package org.mediscan.client.model + +data class AiServiceClientResult( + val rank: Long, + val code: String, + val accuracy: Double, +) diff --git a/core/core-api/build.gradle.kts b/core/core-api/build.gradle.kts index 9725376..42af19a 100644 --- a/core/core-api/build.gradle.kts +++ b/core/core-api/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { implementation(project(":support:monitoring")) implementation(project(":support:logging")) implementation(project(":storage:db-core")) - implementation(project(":clients:client-example")) + implementation(project(":clients:client")) testImplementation(project(":tests:api-docs")) diff --git a/core/core-api/src/main/kotlin/org/mediscan/core/api/controller/v1/response/PillIdentificationResponseDto.kt b/core/core-api/src/main/kotlin/org/mediscan/core/api/controller/v1/response/PillIdentificationResponseDto.kt index f0a378d..29d01ff 100644 --- a/core/core-api/src/main/kotlin/org/mediscan/core/api/controller/v1/response/PillIdentificationResponseDto.kt +++ b/core/core-api/src/main/kotlin/org/mediscan/core/api/controller/v1/response/PillIdentificationResponseDto.kt @@ -2,6 +2,7 @@ package org.mediscan.core.api.controller.v1.response data class PillIdentificationResponseDto( val pillId: String, + val rank: Long, val pillName: String?, val itemImage: String?, val className: String?, diff --git a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillIdentificationService.kt b/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillIdentificationService.kt deleted file mode 100644 index 843d645..0000000 --- a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillIdentificationService.kt +++ /dev/null @@ -1,29 +0,0 @@ -package org.mediscan.core.api.domain - -import org.mediscan.client.example.AiServiceClient -import org.mediscan.client.example.model.PillIdentificationRequestDto -import org.mediscan.client.example.model.PillIdentificationResponseDto -import org.springframework.stereotype.Service -import org.springframework.web.multipart.MultipartFile - -@Service -class PillIdentificationService( - private val aiServiceClient: AiServiceClient, -) { - fun identifyPill( - frontImage: MultipartFile, - backImage: MultipartFile, - pillShape: String, - frontMarking: String = "None", - backMarking: String = "None", - ): List { - val request = PillIdentificationRequestDto( - frontImage = frontImage, - backImage = backImage, - pillShape = pillShape, - frontMarking = frontMarking, - backMarking = backMarking, - ) - return aiServiceClient.identifyPill(request) - } -} diff --git a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillManager.kt b/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillManager.kt index e96bdc1..c76cfd2 100644 --- a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillManager.kt +++ b/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillManager.kt @@ -2,27 +2,29 @@ package org.mediscan.core.api.domain import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVPrinter -import org.apache.commons.io.output.ByteArrayOutputStream import org.springframework.stereotype.Component -import java.io.OutputStreamWriter +import java.io.File +import java.io.FileWriter @Component class PillManager { - fun createCsvInMemory(pillShape: String, pillFrontMarking: String?, pillBackMarking: String?): ByteArray { - val outputStream = ByteArrayOutputStream() - val writer = OutputStreamWriter(outputStream) + fun createCsvFile(pillShape: String, pillFrontMarking: String?, pillBackMarking: String?, filePath: String) { + val file = File(filePath) + val parentDir = file.parentFile + if (!parentDir.exists()) { + parentDir.mkdirs() + } + val writer = FileWriter(file) val csvPrinter = - CSVPrinter(writer, CSVFormat.DEFAULT.builder().setHeader("shape", "f_text", "b_text", "drug_code").build()) + CSVPrinter(writer, CSVFormat.DEFAULT.builder().setHeader("shape", "f_text", "b_text", "drug_code").setDelimiter('\t').build()) csvPrinter.printRecord( pillShape, - pillFrontMarking.let { it ?: "none" }, - pillBackMarking.let { it ?: "none" }, + pillFrontMarking ?: "none", + pillBackMarking ?: "none", "none", ) csvPrinter.flush() csvPrinter.close() - - return outputStream.toByteArray() } } diff --git a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillService.kt b/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillService.kt index 5187e7a..9276acd 100644 --- a/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillService.kt +++ b/core/core-api/src/main/kotlin/org/mediscan/core/api/domain/PillService.kt @@ -1,38 +1,55 @@ package org.mediscan.core.api.domain +import org.mediscan.client.AiServiceClient import org.mediscan.core.api.controller.v1.response.PillDetailResponseDto import org.mediscan.core.api.controller.v1.response.PillIdentificationResponseDto import org.mediscan.core.api.controller.v1.response.PillSearchResponseDto +import org.mediscan.core.api.support.utils.FileMultipartFile import org.mediscan.core.enums.Color import org.springframework.stereotype.Service +import java.io.File @Service class PillService( private val pillManager: PillManager, private val pillReader: PillReader, + private val aiServiceClient: AiServiceClient, ) { fun identifyPill( pillIdentificationData: PillIdentificationData, ): List { - val createCsvInMemory = pillManager.createCsvInMemory( + val csvFileName = "./data/pill.csv" + + pillManager.createCsvFile( pillIdentificationData.pillShape, pillIdentificationData.frontMarking, pillIdentificationData.backMarking, + csvFileName, ) - var result: MutableList = mutableListOf() - - val rank1: Pill = pillReader.readPill("198600161") - val rank2: Pill = pillReader.readPill("199301111") - val rank3: Pill = pillReader.readPill("199302061") - val rank4: Pill = pillReader.readPill("199501735") - val rank5: Pill = pillReader.readPill("199302671") + try { + val aiServiceClientResult = aiServiceClient.predict( + FileMultipartFile(File(csvFileName), "text/csv"), + pillIdentificationData.frontImage, + pillIdentificationData.backImage, + ) + val responseDtoList = mutableListOf() + for (result in aiServiceClientResult) { + val pill = pillReader.readPill(result.code) + val responseDto = PillIdentificationResponseDto( + pill.itemSeq, + result.rank, + pill.itemName, + pill.itemImage, + pill.className, + (result.accuracy * 100).toLong(), + ) + responseDtoList.add(responseDto) + } - result.add(PillIdentificationResponseDto(rank1.itemSeq, rank1.itemName, rank1.itemImage, rank1.className, 60)) - result.add(PillIdentificationResponseDto(rank2.itemSeq, rank2.itemName, rank2.itemImage, rank2.className, 56)) - result.add(PillIdentificationResponseDto(rank3.itemSeq, rank3.itemName, rank3.itemImage, rank3.className, 55)) - result.add(PillIdentificationResponseDto(rank4.itemSeq, rank4.itemName, rank4.itemImage, rank4.className, 47)) - result.add(PillIdentificationResponseDto(rank5.itemSeq, rank5.itemName, rank5.itemImage, rank5.className, 47)) - return result + return responseDtoList + } finally { + File(csvFileName).delete() + } } fun searchPill( diff --git a/core/core-api/src/main/kotlin/org/mediscan/core/api/support/utils/FileMultipartFile.kt b/core/core-api/src/main/kotlin/org/mediscan/core/api/support/utils/FileMultipartFile.kt new file mode 100644 index 0000000..26cfa18 --- /dev/null +++ b/core/core-api/src/main/kotlin/org/mediscan/core/api/support/utils/FileMultipartFile.kt @@ -0,0 +1,44 @@ +package org.mediscan.core.api.support.utils + +import org.springframework.web.multipart.MultipartFile +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +class FileMultipartFile( + private val file: File, + private val contentType: String, +) : MultipartFile { + + override fun getName(): String { + return file.name + } + + override fun getOriginalFilename(): String { + return file.name + } + + override fun getContentType(): String { + return contentType + } + + override fun isEmpty(): Boolean { + return file.length() == 0L + } + + override fun getSize(): Long { + return file.length() + } + + override fun getBytes(): ByteArray { + return file.readBytes() + } + + override fun getInputStream(): InputStream { + return FileInputStream(file) + } + + override fun transferTo(dest: File) { + file.copyTo(dest, overwrite = true) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 66fe8e3..f301504 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,7 @@ include( "tests:api-docs", "support:logging", "support:monitoring", - "clients:client-example" + "clients:client", ) pluginManagement {