From a4f3ba64bf5c5de0b7ccf0579b800c5a61e8e2da Mon Sep 17 00:00:00 2001 From: Vladysl <45620393+Vladysl@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:47:39 +0200 Subject: [PATCH] 1596 - implementation of genAI service support (#1613) --- gradle/libs.versions.toml | 5 +- .../config/WebClientConfiguration.java | 29 ++++++++++ .../controller/GenAIController.java | 24 ++++++++ .../exception/ControllerAdvice.java | 7 +++ .../oddplatform/exception/GenAIException.java | 12 ++++ .../service/genai/GenAIService.java | 9 +++ .../service/genai/GenAIServiceImpl.java | 57 +++++++++++++++++++ .../src/main/resources/application.yml | 4 ++ odd-platform-specification/components.yaml | 12 ++++ odd-platform-specification/openapi.yaml | 24 +++++++- 10 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/GenAIController.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/exception/GenAIException.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIService.java create mode 100644 odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b69b9483a..e4681d38b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ oddrn-generator-java = '0.1.21' odd-integration-manifests = '0.0.6' apache-collections = '4.4' apache-lang = '3.12.0' +apache-text = '1.9' r2dbc-spi = '1.0.0.RELEASE' r2dbc-postgresql = '1.0.1.RELEASE' r2dbc-pool = '1.0.0.RELEASE' @@ -67,6 +68,7 @@ oddrn-generator-java = { module = 'org.opendatadiscovery:oddrn-generator-java', odd-integration-manifests = { module = 'org.opendatadiscovery:integration-manifests', version.ref = 'odd-integration-manifests' } apache-collections = { module = 'org.apache.commons:commons-collections4', version.ref = 'apache-collections' } apache-lang = { module = 'org.apache.commons:commons-lang3', version.ref = 'apache-lang' } +apache-text = { module = 'org.apache.commons:commons-text', version.ref = 'apache-text' } r2dbc-spi = { module = 'io.r2dbc:r2dbc-spi', version.ref = 'r2dbc-spi' } r2dbc-postgresql = { module = 'org.postgresql:r2dbc-postgresql', version.ref = 'r2dbc-postgresql' } r2dbc-pool = { module = 'io.r2dbc:r2dbc-pool', version.ref = 'r2dbc-pool' } @@ -142,7 +144,8 @@ internal = [ ] apache-commons = [ 'apache-collections', - 'apache-lang' + 'apache-lang', + 'apache-text' ] r2dbc = [ 'r2dbc-spi', diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java new file mode 100644 index 000000000..13c88413c --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/config/WebClientConfiguration.java @@ -0,0 +1,29 @@ +package org.opendatadiscovery.oddplatform.config; + +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +@Configuration +public class WebClientConfiguration { + @Value("${genai.url:}") + private String genAIUrl; + @Value("${genai.request_timeout:2}") + private Integer getAiRequestTimeout; + + @Bean("genAiWebClient") + public WebClient webClient() { + final HttpClient httpClient = HttpClient.create().responseTimeout(Duration.ofMinutes(getAiRequestTimeout)); + final ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); + + return WebClient.builder() + .clientConnector(connector) + .baseUrl(genAIUrl) + .build(); + } +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/GenAIController.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/GenAIController.java new file mode 100644 index 000000000..c22143670 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/GenAIController.java @@ -0,0 +1,24 @@ +package org.opendatadiscovery.oddplatform.controller; + +import lombok.RequiredArgsConstructor; +import org.opendatadiscovery.oddplatform.api.contract.api.GenaiApi; +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIRequest; +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIResponse; +import org.opendatadiscovery.oddplatform.service.genai.GenAIService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +@RequiredArgsConstructor +public class GenAIController implements GenaiApi { + private final GenAIService genAIService; + + @Override + public Mono> genAiQuestion(final Mono genAIRequest, + final ServerWebExchange exchange) { + return genAIRequest.flatMap(genAIService::getResponseFromGenAI) + .map(ResponseEntity::ok); + } +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/exception/ControllerAdvice.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/exception/ControllerAdvice.java index 57caaf817..f5e79d712 100644 --- a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/exception/ControllerAdvice.java +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/controller/exception/ControllerAdvice.java @@ -8,6 +8,7 @@ import org.opendatadiscovery.oddplatform.exception.CascadeDeleteException; import org.opendatadiscovery.oddplatform.exception.ErrorCode; import org.opendatadiscovery.oddplatform.exception.ExceptionWithErrorCode; +import org.opendatadiscovery.oddplatform.exception.GenAIException; import org.opendatadiscovery.oddplatform.exception.NotFoundException; import org.opendatadiscovery.oddplatform.exception.UniqueConstraintException; import org.springframework.http.HttpStatus; @@ -51,6 +52,12 @@ public ErrorResponse handleException(final WebExchangeBindException e) { return buildResponse(e); } + @ExceptionHandler(GenAIException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleGenAIException(final GenAIException e) { + return buildResponse(e); + } + @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleServerException(final Exception e) { diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/exception/GenAIException.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/exception/GenAIException.java new file mode 100644 index 000000000..d6b1799ae --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/exception/GenAIException.java @@ -0,0 +1,12 @@ +package org.opendatadiscovery.oddplatform.exception; + +public class GenAIException extends ExceptionWithErrorCode { + + public GenAIException(final String message) { + super(ErrorCode.SERVER_EXCEPTION, message); + } + + public GenAIException(final Throwable e) { + super(ErrorCode.SERVER_EXCEPTION, e.getMessage()); + } +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIService.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIService.java new file mode 100644 index 000000000..e1aa2182f --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIService.java @@ -0,0 +1,9 @@ +package org.opendatadiscovery.oddplatform.service.genai; + +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIRequest; +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIResponse; +import reactor.core.publisher.Mono; + +public interface GenAIService { + Mono getResponseFromGenAI(final GenAIRequest item); +} diff --git a/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java new file mode 100644 index 000000000..37b384089 --- /dev/null +++ b/odd-platform-api/src/main/java/org/opendatadiscovery/oddplatform/service/genai/GenAIServiceImpl.java @@ -0,0 +1,57 @@ +package org.opendatadiscovery.oddplatform.service.genai; + +import com.google.common.base.CharMatcher; +import io.netty.handler.timeout.ReadTimeoutException; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIRequest; +import org.opendatadiscovery.oddplatform.api.contract.model.GenAIResponse; +import org.opendatadiscovery.oddplatform.exception.BadUserRequestException; +import org.opendatadiscovery.oddplatform.exception.GenAIException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Slf4j +@Service +public class GenAIServiceImpl implements GenAIService { + public static final String QUERY_DATA = "/query_data"; + public static final String QUESTION_FIELD = "question"; + + private final String genAIUrl; + private final Integer getAiRequestTimeout; + private final WebClient webClient; + + @Autowired + public GenAIServiceImpl(@Value("${genai.url:}") final String genAIUrl, + @Value("${genai.request_timeout:2}") final Integer getAiRequestTimeout, + @Qualifier("genAiWebClient") final WebClient webClient) { + this.genAIUrl = genAIUrl; + this.getAiRequestTimeout = getAiRequestTimeout; + this.webClient = webClient; + } + + @Override + public Mono getResponseFromGenAI(final GenAIRequest request) { + if (StringUtils.isBlank(genAIUrl)) { + return Mono.error(new BadUserRequestException("Gen AI is disabled")); + } + + return webClient.post() + .uri(QUERY_DATA) + .bodyValue(Map.of(QUESTION_FIELD, request.getBody())) + .retrieve() + .bodyToMono(String.class) + .map(item -> new GenAIResponse() + .body(StringEscapeUtils.unescapeJava(CharMatcher.is('\"').trimFrom(item)))) + .onErrorResume(e -> e.getCause() instanceof ReadTimeoutException + ? Mono.error(new GenAIException( + "Gen AI request take longer that %s min".formatted(getAiRequestTimeout))) + : Mono.error(new GenAIException(e))); + } +} diff --git a/odd-platform-api/src/main/resources/application.yml b/odd-platform-api/src/main/resources/application.yml index f1f386e9d..c6bdd25bb 100644 --- a/odd-platform-api/src/main/resources/application.yml +++ b/odd-platform-api/src/main/resources/application.yml @@ -14,6 +14,10 @@ spring: codec: max-in-memory-size: 20MB +#genai: +# url: http://localhost:5000 +# request_timeout: 2 + springdoc: api-docs: path: /api/v3/swagger-ui.html diff --git a/odd-platform-specification/components.yaml b/odd-platform-specification/components.yaml index 93321f9d7..804c773e9 100644 --- a/odd-platform-specification/components.yaml +++ b/odd-platform-specification/components.yaml @@ -4063,6 +4063,18 @@ components: - GRAPH - ALL + GenAIRequest: + type: object + properties: + body: + type: string + + GenAIResponse: + type: object + properties: + body: + type: string + parameters: PageParam: name: page diff --git a/odd-platform-specification/openapi.yaml b/odd-platform-specification/openapi.yaml index 31b88aebc..fc1659db3 100644 --- a/odd-platform-specification/openapi.yaml +++ b/odd-platform-specification/openapi.yaml @@ -44,6 +44,7 @@ tags: - name: dataQualityRuns - name: queryExample - name: referenceData + - name: genai paths: /api/integrations: @@ -3883,4 +3884,25 @@ paths: schema: $ref: './components.yaml/#/components/schemas/DataEntityGraphRelationshipDetails' tags: - - relationship \ No newline at end of file + - relationship + + /api/genai/ask: + post: + summary: Request to genai service + description: create request to genai service with specific question + operationId: genAiQuestion + requestBody: + required: true + content: + application/json: + schema: + $ref: './components.yaml/#/components/schemas/GenAIRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: './components.yaml/#/components/schemas/GenAIResponse' + tags: + - genai \ No newline at end of file