From 9f18727458e96372fe1aefe540f9832d4c2ef76b Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 01:18:04 +0200 Subject: [PATCH 01/23] feat: update chat endpoints default values --- .../antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc | 4 ++-- .../watsonxai/WatsonxAiConnectionProperties.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc index 4b085b72b81..0850399742b 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc @@ -45,8 +45,8 @@ The prefix `spring.ai.watsonx.ai` is used as the property prefix that lets you c | Property | Description | Default | spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com -| spring.ai.watsonx.ai.stream-endpoint | The streaming endpoint | generation/stream?version=2023-05-29 -| spring.ai.watsonx.ai.text-endpoint | The text endpoint | generation/text?version=2023-05-29 +| spring.ai.watsonx.ai.stream-endpoint | The streaming endpoint | ml/v1/text/generation_stream?version=2023-05-29 +| spring.ai.watsonx.ai.text-endpoint | The text endpoint | ml/v1/text/generation?version=2023-05-29 | spring.ai.watsonx.ai.project-id | The project ID | - | spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - |==== diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java index 3e6357c6332..a565b200c48 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java @@ -31,9 +31,9 @@ public class WatsonxAiConnectionProperties { private String baseUrl = "https://us-south.ml.cloud.ibm.com/"; - private String streamEndpoint = "generation/stream?version=2023-05-29"; + private String streamEndpoint = "ml/v1/text/generation_stream?version=2023-05-29"; - private String textEndpoint = "generation/text?version=2023-05-29"; + private String textEndpoint = "ml/v1/text/generation?version=2023-05-29"; private String projectId; From 118f282888c7d82466c94eb97f442c1e90423031 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 01:20:22 +0200 Subject: [PATCH 02/23] feat: add embedding endpoint to watsonx connection properties --- .../autoconfigure/watsonxai/WatsonxAiConnectionProperties.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java index a565b200c48..2832370418d 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java @@ -35,6 +35,8 @@ public class WatsonxAiConnectionProperties { private String textEndpoint = "ml/v1/text/generation?version=2023-05-29"; + private String embeddingEndpoint = "ml/v1/embeddings?version=2023-05-29"; + private String projectId; private String IAMToken; From 546b724dc04d828ab50755e5eb2964ea641b03ac Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 01:21:51 +0200 Subject: [PATCH 03/23] feat: add getter and setter for the embedding endpoint parameter --- .../watsonxai/WatsonxAiConnectionProperties.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java index 2832370418d..62c8ea07b99 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java @@ -65,6 +65,14 @@ public void setTextEndpoint(String textEndpoint) { this.textEndpoint = textEndpoint; } + public String getEmbeddingEndpoint() { + return embeddingEndpoint; + } + + public void setEmbeddingEndpoint(String embeddingEndpoint) { + this.embeddingEndpoint = embeddingEndpoint; + } + public String getProjectId() { return projectId; } From 1e4535e5b9561c9a5df34825e432e0751b7daf3c Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 01:29:43 +0200 Subject: [PATCH 04/23] feat: add embedding endpoint into watsonx api --- .../ai/watsonx/api/WatsonxAiApi.java | 4 ++++ .../watsonxai/WatsonxAiAutoConfiguration.java | 3 ++- .../watsonxai/WatsonxAiAutoConfigurationTests.java | 13 +++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java index 30673ce617b..71a31370e87 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java @@ -52,6 +52,7 @@ public class WatsonxAiApi { private final IamAuthenticator iamAuthenticator; private final String streamEndpoint; private final String textEndpoint; + private final String embeddingEndpoint; private final String projectId; private IamToken token; @@ -60,6 +61,7 @@ public class WatsonxAiApi { * @param baseUrl api base URL. * @param streamEndpoint streaming generation. * @param textEndpoint text generation. + * @param embeddingEndpoint embedding generation * @param projectId watsonx.ai project identifier. * @param IAMToken IBM Cloud IAM token. * @param restClientBuilder rest client builder. @@ -68,12 +70,14 @@ public WatsonxAiApi( String baseUrl, String streamEndpoint, String textEndpoint, + String embeddingEndpoint, String projectId, String IAMToken, RestClient.Builder restClientBuilder ) { this.streamEndpoint = streamEndpoint; this.textEndpoint = textEndpoint; + this.embeddingEndpoint = embeddingEndpoint; this.projectId = projectId; this.iamAuthenticator = IamAuthenticator.fromConfiguration(Map.of("APIKEY", IAMToken)); this.token = this.iamAuthenticator.requestToken(); diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java index fd62de3cebb..e640f84c779 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java @@ -45,7 +45,8 @@ public class WatsonxAiAutoConfiguration { @ConditionalOnMissingBean public WatsonxAiApi watsonxApi(WatsonxAiConnectionProperties properties, RestClient.Builder restClientBuilder) { return new WatsonxAiApi(properties.getBaseUrl(), properties.getStreamEndpoint(), properties.getTextEndpoint(), - properties.getProjectId(), properties.getIAMToken(), restClientBuilder); + properties.getEmbeddingEndpoint(), properties.getProjectId(), properties.getIAMToken(), + restClientBuilder); } @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java index 6a8831cfe5c..1637b204ebf 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java @@ -29,8 +29,9 @@ public void propertiesTest() { new ApplicationContextRunner().withPropertyValues( // @formatter:off "spring.ai.watsonx.ai.base-url=TEST_BASE_URL", - "spring.ai.watsonx.ai.stream-endpoint=generation/stream?version=2023-05-29", - "spring.ai.watsonx.ai.text-endpoint=generation/text?version=2023-05-29", + "spring.ai.watsonx.ai.stream-endpoint=ml/v1/text/generation_stream?version=2023-05-29", + "spring.ai.watsonx.ai.text-endpoint=ml/v1/text/generation?version=2023-05-29", + "spring.ai.watsonx.ai.embedding-endpoint=ml/v1/text/embeddings?version=2023-05-29", "spring.ai.watsonx.ai.projectId=1", "spring.ai.watsonx.ai.IAMToken=123456") // @formatter:on @@ -39,8 +40,12 @@ public void propertiesTest() { .run(context -> { var connectionProperties = context.getBean(WatsonxAiConnectionProperties.class); assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL"); - assertThat(connectionProperties.getStreamEndpoint()).isEqualTo("generation/stream?version=2023-05-29"); - assertThat(connectionProperties.getTextEndpoint()).isEqualTo("generation/text?version=2023-05-29"); + assertThat(connectionProperties.getStreamEndpoint()) + .isEqualTo("ml/v1/text/generation_stream?version=2023-05-29"); + assertThat(connectionProperties.getTextEndpoint()) + .isEqualTo("ml/v1/text/generation?version=2023-05-29"); + assertThat(connectionProperties.getEmbeddingEndpoint()) + .isEqualTo("ml/v1/text/embeddings?version=2023-05-29"); assertThat(connectionProperties.getProjectId()).isEqualTo("1"); assertThat(connectionProperties.getIAMToken()).isEqualTo("123456"); }); From 6c4f282321a3ee0a4f327a6409598055d237f531 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 01:34:44 +0200 Subject: [PATCH 05/23] feat: add embedding endpoint info --- .../modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc index 0850399742b..8d5837c773c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc @@ -44,11 +44,12 @@ The prefix `spring.ai.watsonx.ai` is used as the property prefix that lets you c |==== | Property | Description | Default -| spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com -| spring.ai.watsonx.ai.stream-endpoint | The streaming endpoint | ml/v1/text/generation_stream?version=2023-05-29 -| spring.ai.watsonx.ai.text-endpoint | The text endpoint | ml/v1/text/generation?version=2023-05-29 -| spring.ai.watsonx.ai.project-id | The project ID | - -| spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - +| spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com +| spring.ai.watsonx.ai.stream-endpoint | The streaming endpoint | ml/v1/text/generation_stream?version=2023-05-29 +| spring.ai.watsonx.ai.text-endpoint | The text endpoint | ml/v1/text/generation?version=2023-05-29 +| spring.ai.watsonx.ai.embedding-endpoint | The embedding endpoint | ml/v1/embeddings?version=2023-05-29 +| spring.ai.watsonx.ai.project-id | The project ID | - +| spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - |==== ==== Configuration Properties From 1d978e8fd7753655de198e452b565c44f8eca046 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 02:45:11 +0200 Subject: [PATCH 06/23] feat: add embedding api call & request and response classes --- .../ai/watsonx/api/WatsonxAiApi.java | 17 +++++ .../api/WatsonxAiEmbeddingRequest.java | 65 +++++++++++++++++++ .../api/WatsonxAiEmbeddingResponse.java | 13 ++++ 3 files changed, 95 insertions(+) create mode 100644 models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java create mode 100644 models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java index 71a31370e87..8c637429312 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java @@ -135,4 +135,21 @@ public Flux generateStreaming(WatsonxAiRequest watsonxAiReque }); } + @Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(random = true, delay = 1200, maxDelay = 7000, multiplier = 2.5)) + public ResponseEntity embeddings(WatsonxAiEmbeddingRequest request) { + Assert.notNull(request, WATSONX_REQUEST_CANNOT_BE_NULL); + + if(this.token.needsRefresh()) { + this.token = this.iamAuthenticator.requestToken(); + } + + return this.restClient.post() + .uri(this.embeddingEndpoint) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + this.token.getAccessToken()) + .body(request.withProjectId(projectId)) + .retrieve() + .toEntity(WatsonxAiEmbeddingResponse.class); + } + + } diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java new file mode 100644 index 00000000000..08b0f83cf7c --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java @@ -0,0 +1,65 @@ +package org.springframework.ai.watsonx.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.ai.watsonx.WatsonxAiEmbeddingOptions; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class WatsonxAiEmbeddingRequest { + + @JsonProperty("model_id") + String model; + + @JsonProperty("inputs") + List inputs; + + @JsonProperty("project_id") + String projectId; + + public String getModel() { + return model; + } + + public List getInputs() { + return inputs; + } + + private WatsonxAiEmbeddingRequest(String model, List inputs, String projectId) { + this.model = model; + this.inputs = inputs; + this.projectId = projectId; + } + + public WatsonxAiEmbeddingRequest withProjectId(String projectId) { + this.projectId = projectId; + return this; + } + + public static Builder builder(List inputs) { + return new Builder(inputs); + } + + public static class Builder { + + private String model = WatsonxAiEmbeddingOptions.DEFAULT_MODEL; + + private final List inputs; + + public Builder(List inputs) { + this.inputs = inputs; + } + + public Builder withModel(String model) { + this.model = model; + return this; + } + + public WatsonxAiEmbeddingRequest build() { + return new WatsonxAiEmbeddingRequest(model, inputs, ""); + } + + } + +} diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java new file mode 100644 index 00000000000..1b8c77030d8 --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java @@ -0,0 +1,13 @@ +package org.springframework.ai.watsonx.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Date; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record WatsonxAiEmbeddingResponse(@JsonProperty("model_id") String model, + @JsonProperty("created_at") Date createdAt, @JsonProperty("results") List results, + @JsonProperty("input_token_count") Integer inputTokenCount) { +} From fa50a5508c06ad1a467090c3705d6f56d42541b4 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 02:46:50 +0200 Subject: [PATCH 07/23] feat: add embedding result record --- .../ai/watsonx/api/WatsonxAiEmbeddingResults.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java new file mode 100644 index 00000000000..362b117c6a6 --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java @@ -0,0 +1,10 @@ +package org.springframework.ai.watsonx.api; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record WatsonxAiEmbeddingResults(@JsonProperty("embedding") List embedding) { +} From 70fc2ea664340e2dd7920920a8378dbef755cb62 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 02:51:06 +0200 Subject: [PATCH 08/23] feat: rename chat classes/types for better understanding and clarity --- .../ai/watsonx/WatsonxAiChatModel.java | 16 +++++----- .../ai/watsonx/api/WatsonxAiApi.java | 16 +++++----- ...Request.java => WatsonxAiChatRequest.java} | 12 +++---- ...sponse.java => WatsonxAiChatResponse.java} | 4 +-- ...Results.java => WatsonxAiChatResults.java} | 2 +- .../ai/watsonx/WatsonxAiChatModelTest.java | 32 +++++++++---------- 6 files changed, 41 insertions(+), 41 deletions(-) rename models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/{WatsonxAiRequest.java => WatsonxAiChatRequest.java} (85%) rename models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/{WatsonxAiResponse.java => WatsonxAiChatResponse.java} (90%) rename models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/{WatsonxAiResults.java => WatsonxAiChatResults.java} (96%) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatModel.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatModel.java index 91a0cda70f2..62a2734b329 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatModel.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiChatModel.java @@ -29,8 +29,8 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.watsonx.api.WatsonxAiApi; -import org.springframework.ai.watsonx.api.WatsonxAiRequest; -import org.springframework.ai.watsonx.api.WatsonxAiResponse; +import org.springframework.ai.watsonx.api.WatsonxAiChatRequest; +import org.springframework.ai.watsonx.api.WatsonxAiChatResponse; import org.springframework.ai.watsonx.utils.MessageToPromptConverter; import org.springframework.util.Assert; @@ -78,9 +78,9 @@ public WatsonxAiChatModel(WatsonxAiApi watsonxAiApi, WatsonxAiChatOptions defaul @Override public ChatResponse call(Prompt prompt) { - WatsonxAiRequest request = request(prompt); + WatsonxAiChatRequest request = request(prompt); - WatsonxAiResponse response = this.watsonxAiApi.generate(request).getBody(); + WatsonxAiChatResponse response = this.watsonxAiApi.generate(request).getBody(); var generator = new Generation(response.results().get(0).generatedText()); generator = generator.withGenerationMetadata( @@ -92,9 +92,9 @@ public ChatResponse call(Prompt prompt) { @Override public Flux stream(Prompt prompt) { - WatsonxAiRequest request = request(prompt); + WatsonxAiChatRequest request = request(prompt); - Flux response = this.watsonxAiApi.generateStreaming(request); + Flux response = this.watsonxAiApi.generateStreaming(request); return response.map(chunk -> { Generation generation = new Generation(chunk.results().get(0).generatedText()); @@ -106,7 +106,7 @@ public Flux stream(Prompt prompt) { }); } - public WatsonxAiRequest request(Prompt prompt) { + public WatsonxAiChatRequest request(Prompt prompt) { WatsonxAiChatOptions options = WatsonxAiChatOptions.builder().build(); @@ -133,7 +133,7 @@ public WatsonxAiRequest request(Prompt prompt) { .withHumanPrompt("") .toPrompt(prompt.getInstructions()); - return WatsonxAiRequest.builder(convertedPrompt).withParameters(parameters).build(); + return WatsonxAiChatRequest.builder(convertedPrompt).withParameters(parameters).build(); } @Override diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java index 8c637429312..2de2f36fd06 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiApi.java @@ -98,8 +98,8 @@ public WatsonxAiApi( } @Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(random = true, delay = 1200, maxDelay = 7000, multiplier = 2.5)) - public ResponseEntity generate(WatsonxAiRequest watsonxAiRequest) { - Assert.notNull(watsonxAiRequest, WATSONX_REQUEST_CANNOT_BE_NULL); + public ResponseEntity generate(WatsonxAiChatRequest watsonxAiChatRequest) { + Assert.notNull(watsonxAiChatRequest, WATSONX_REQUEST_CANNOT_BE_NULL); if(this.token.needsRefresh()) { this.token = this.iamAuthenticator.requestToken(); @@ -108,14 +108,14 @@ public ResponseEntity generate(WatsonxAiRequest watsonxAiRequ return this.restClient.post() .uri(this.textEndpoint) .header(HttpHeaders.AUTHORIZATION, "Bearer " + this.token.getAccessToken()) - .body(watsonxAiRequest.withProjectId(projectId)) + .body(watsonxAiChatRequest.withProjectId(projectId)) .retrieve() - .toEntity(WatsonxAiResponse.class); + .toEntity(WatsonxAiChatResponse.class); } @Retryable(retryFor = Exception.class, maxAttempts = 3, backoff = @Backoff(random = true, delay = 1200, maxDelay = 7000, multiplier = 2.5)) - public Flux generateStreaming(WatsonxAiRequest watsonxAiRequest) { - Assert.notNull(watsonxAiRequest, WATSONX_REQUEST_CANNOT_BE_NULL); + public Flux generateStreaming(WatsonxAiChatRequest watsonxAiChatRequest) { + Assert.notNull(watsonxAiChatRequest, WATSONX_REQUEST_CANNOT_BE_NULL); if(this.token.needsRefresh()) { this.token = this.iamAuthenticator.requestToken(); @@ -124,9 +124,9 @@ public Flux generateStreaming(WatsonxAiRequest watsonxAiReque return this.webClient.post() .uri(this.streamEndpoint) .header(HttpHeaders.AUTHORIZATION, "Bearer " + this.token.getAccessToken()) - .bodyValue(watsonxAiRequest.withProjectId(this.projectId)) + .bodyValue(watsonxAiChatRequest.withProjectId(this.projectId)) .retrieve() - .bodyToFlux(WatsonxAiResponse.class) + .bodyToFlux(WatsonxAiChatResponse.class) .handle((data, sink) -> { if (logger.isTraceEnabled()) { logger.trace(data); diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiRequest.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java similarity index 85% rename from models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiRequest.java rename to models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java index 2ca88e25864..40b0c53ab83 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiRequest.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java @@ -25,7 +25,7 @@ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) -public class WatsonxAiRequest { +public class WatsonxAiChatRequest { @JsonProperty("input") private String input; @@ -36,19 +36,19 @@ public class WatsonxAiRequest { @JsonProperty("project_id") private String projectId = ""; - private WatsonxAiRequest(String input, Map parameters, String modelId, String projectId) { + private WatsonxAiChatRequest(String input, Map parameters, String modelId, String projectId) { this.input = input; this.parameters = parameters; this.modelId = modelId; this.projectId = projectId; } - public WatsonxAiRequest withModelId(String modelId) { + public WatsonxAiChatRequest withModelId(String modelId) { this.modelId = modelId; return this; } - public WatsonxAiRequest withProjectId(String projectId) { + public WatsonxAiChatRequest withProjectId(String projectId) { this.projectId = projectId; return this; } @@ -79,8 +79,8 @@ public Builder withParameters(Map parameters) { return this; } - public WatsonxAiRequest build() { - return new WatsonxAiRequest(input, parameters, model, ""); + public WatsonxAiChatRequest build() { + return new WatsonxAiChatRequest(input, parameters, model, ""); } } diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResponse.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java similarity index 90% rename from models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResponse.java rename to models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java index dd776626867..f2db26ea1cb 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResponse.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java @@ -24,9 +24,9 @@ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) -public record WatsonxAiResponse( +public record WatsonxAiChatResponse( @JsonProperty("model_id") String modelId, @JsonProperty("created_at") Date createdAt, - @JsonProperty("results") List results, + @JsonProperty("results") List results, @JsonProperty("system") Map system ) {} diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResults.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java similarity index 96% rename from models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResults.java rename to models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java index a0d28b90060..ef21689180d 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiResults.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java @@ -20,7 +20,7 @@ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) -public record WatsonxAiResults( +public record WatsonxAiChatResults( @JsonProperty("generated_text") String generatedText, @JsonProperty("generated_token_count") Integer generatedTokenCount, @JsonProperty("input_token_count") Integer inputTokenCount, diff --git a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java index cf4f1c739d1..a2654b8b1c7 100644 --- a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java +++ b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java @@ -32,9 +32,9 @@ import org.springframework.ai.chat.prompt.ChatOptionsBuilder; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.watsonx.api.WatsonxAiApi; -import org.springframework.ai.watsonx.api.WatsonxAiRequest; -import org.springframework.ai.watsonx.api.WatsonxAiResponse; -import org.springframework.ai.watsonx.api.WatsonxAiResults; +import org.springframework.ai.watsonx.api.WatsonxAiChatRequest; +import org.springframework.ai.watsonx.api.WatsonxAiChatResponse; +import org.springframework.ai.watsonx.api.WatsonxAiChatResults; import org.springframework.http.ResponseEntity; import static org.assertj.core.api.Assertions.assertThat; @@ -57,7 +57,7 @@ public void testCreateRequestWithNoModelId() { Prompt prompt = new Prompt("Test message", options); Exception exception = Assert.assertThrows(IllegalArgumentException.class, () -> { - WatsonxAiRequest request = chatModel.request(prompt); + WatsonxAiChatRequest request = chatModel.request(prompt); }); } @@ -71,7 +71,7 @@ public void testCreateRequestSuccessfullyWithDefaultParams() { .build(); Prompt prompt = new Prompt(msg, modelOptions); - WatsonxAiRequest request = chatModel.request(prompt); + WatsonxAiChatRequest request = chatModel.request(prompt); Assert.assertEquals(request.getModelId(), "meta-llama/llama-2-70b-chat"); assertThat(request.getParameters().get("decoding_method")).isEqualTo("greedy"); @@ -105,7 +105,7 @@ public void testCreateRequestSuccessfullyWithNonDefaultParams() { Prompt prompt = new Prompt(msg, modelOptions); - WatsonxAiRequest request = chatModel.request(prompt); + WatsonxAiChatRequest request = chatModel.request(prompt); Assert.assertEquals(request.getModelId(), "meta-llama/llama-2-70b-chat"); assertThat(request.getParameters().get("decoding_method")).isEqualTo("sample"); @@ -139,7 +139,7 @@ public void testCreateRequestSuccessfullyWithChatDisabled() { Prompt prompt = new Prompt(msg, modelOptions); - WatsonxAiRequest request = chatModel.request(prompt); + WatsonxAiChatRequest request = chatModel.request(prompt); Assert.assertEquals(request.getModelId(), "meta-llama/llama-2-70b-chat"); assertThat(request.getInput()).isEqualTo(msg); @@ -164,12 +164,12 @@ public void testCallMethod() { WatsonxAiChatOptions parameters = WatsonxAiChatOptions.builder().withModel("google/flan-ul2").build(); - WatsonxAiResults fakeResults = new WatsonxAiResults("LLM response", 4, 3, "max_tokens"); + WatsonxAiChatResults fakeResults = new WatsonxAiChatResults("LLM response", 4, 3, "max_tokens"); - WatsonxAiResponse fakeResponse = new WatsonxAiResponse("google/flan-ul2", new Date(), List.of(fakeResults), + WatsonxAiChatResponse fakeResponse = new WatsonxAiChatResponse("google/flan-ul2", new Date(), List.of(fakeResults), Map.of("warnings", List.of(Map.of("message", "the message", "id", "disclaimer_warning")))); - when(mockChatApi.generate(any(WatsonxAiRequest.class))) + when(mockChatApi.generate(any(WatsonxAiChatRequest.class))) .thenReturn(ResponseEntity.of(Optional.of(fakeResponse))); Generation expectedGenerator = new Generation("LLM response") @@ -193,17 +193,17 @@ public void testStreamMethod() { WatsonxAiChatOptions parameters = WatsonxAiChatOptions.builder().withModel("google/flan-ul2").build(); - WatsonxAiResults fakeResultsFirst = new WatsonxAiResults("LLM resp", 0, 0, "max_tokens"); - WatsonxAiResults fakeResultsSecond = new WatsonxAiResults("onse", 4, 3, "not_finished"); + WatsonxAiChatResults fakeResultsFirst = new WatsonxAiChatResults("LLM resp", 0, 0, "max_tokens"); + WatsonxAiChatResults fakeResultsSecond = new WatsonxAiChatResults("onse", 4, 3, "not_finished"); - WatsonxAiResponse fakeResponseFirst = new WatsonxAiResponse("google/flan-ul2", new Date(), + WatsonxAiChatResponse fakeResponseFirst = new WatsonxAiChatResponse("google/flan-ul2", new Date(), List.of(fakeResultsFirst), Map.of("warnings", List.of(Map.of("message", "the message", "id", "disclaimer_warning")))); - WatsonxAiResponse fakeResponseSecond = new WatsonxAiResponse("google/flan-ul2", new Date(), + WatsonxAiChatResponse fakeResponseSecond = new WatsonxAiChatResponse("google/flan-ul2", new Date(), List.of(fakeResultsSecond), null); - Flux fakeResponse = Flux.just(fakeResponseFirst, fakeResponseSecond); - when(mockChatApi.generateStreaming(any(WatsonxAiRequest.class))).thenReturn(fakeResponse); + Flux fakeResponse = Flux.just(fakeResponseFirst, fakeResponseSecond); + when(mockChatApi.generateStreaming(any(WatsonxAiChatRequest.class))).thenReturn(fakeResponse); Generation firstGen = new Generation("LLM resp") .withGenerationMetadata(ChatGenerationMetadata.from("max_tokens", From 9d48f83a7f59de21519d668074f94af7b26afbcf Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:03:36 +0200 Subject: [PATCH 09/23] fix: linter --- .../org/springframework/ai/watsonx/WatsonxAiChatModelTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java index a2654b8b1c7..2d860422f32 100644 --- a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java +++ b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiChatModelTest.java @@ -166,7 +166,8 @@ public void testCallMethod() { WatsonxAiChatResults fakeResults = new WatsonxAiChatResults("LLM response", 4, 3, "max_tokens"); - WatsonxAiChatResponse fakeResponse = new WatsonxAiChatResponse("google/flan-ul2", new Date(), List.of(fakeResults), + WatsonxAiChatResponse fakeResponse = new WatsonxAiChatResponse("google/flan-ul2", new Date(), + List.of(fakeResults), Map.of("warnings", List.of(Map.of("message", "the message", "id", "disclaimer_warning")))); when(mockChatApi.generate(any(WatsonxAiChatRequest.class))) From 7d2ad06288445eded0f8664d5c0bb02c4906e55f Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:03:58 +0200 Subject: [PATCH 10/23] feat: add watsonx embedding options class and tests --- .../ai/watsonx/WatsonxAiEmbeddingOptions.java | 49 +++++++++++++++++++ .../api/WatsonxAiEmbeddingOptionTest.java | 36 ++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java create mode 100644 models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingOptionTest.java diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java new file mode 100644 index 00000000000..71d7ce698b3 --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java @@ -0,0 +1,49 @@ +package org.springframework.ai.watsonx; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.ai.embedding.EmbeddingOptions; + +/** + * The configuration information for the embedding requests. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class WatsonxAiEmbeddingOptions implements EmbeddingOptions { + + public static final String DEFAULT_MODEL = "ibm/slate-30m-english-rtrvr"; + + /** + * The embedding model identifier + */ + @JsonProperty("model_id") + private String model; + + public WatsonxAiEmbeddingOptions withModel(String model) { + this.model = model; + return this; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + /** + * Helper factory method to create a new {@link WatsonxAiEmbeddingOptions} instance. + * @return A new {@link WatsonxAiEmbeddingOptions} instance. + */ + public static WatsonxAiEmbeddingOptions create() { + return new WatsonxAiEmbeddingOptions(); + } + + public static WatsonxAiEmbeddingOptions fromOptions(WatsonxAiEmbeddingOptions fromOptions) { + return new WatsonxAiEmbeddingOptions().withModel(fromOptions.getModel()); + } + +} diff --git a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingOptionTest.java b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingOptionTest.java new file mode 100644 index 00000000000..98d63092f7b --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingOptionTest.java @@ -0,0 +1,36 @@ +package org.springframework.ai.watsonx.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.ai.watsonx.WatsonxAiEmbeddingOptions; + +/** + * @author Pablo Sanchidrian Herrera + */ +public class WatsonxAiEmbeddingOptionTest { + + @Test + public void testWithModel() { + WatsonxAiEmbeddingOptions options = new WatsonxAiEmbeddingOptions(); + options.withModel("test-model"); + assertThat("test-model").isEqualTo(options.getModel()); + } + + @Test + public void testCreateFactoryMethod() { + WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create(); + assertThat(options).isNotNull(); + assertThat(options.getModel()).isNull(); + } + + @Test + public void testFromOptionsFactoryMethod() { + WatsonxAiEmbeddingOptions originalOptions = new WatsonxAiEmbeddingOptions().withModel("original-model"); + WatsonxAiEmbeddingOptions newOptions = WatsonxAiEmbeddingOptions.fromOptions(originalOptions); + + assertThat(newOptions).isNotNull(); + assertThat("original-model").isEqualTo(newOptions.getModel()); + } + +} From aeeb444f3e67f4bfebd8de031ab845fc76bf97cf Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:29:48 +0200 Subject: [PATCH 11/23] feat: add watsonx embedding model --- .../ai/watsonx/WatsonxAiEmbeddingModel.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java new file mode 100644 index 00000000000..6a43c6f7d8b --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java @@ -0,0 +1,86 @@ +package org.springframework.ai.watsonx; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.document.Document; +import org.springframework.ai.embedding.*; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.watsonx.api.WatsonxAiApi; +import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingRequest; +import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingResponse; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@link EmbeddingModel} implementation for {@literal Watsonx.ai}. + * + * Watsonx.ai allows developers to run large language models and generate embeddings. It + * supports open-source models available on [Watsonx.ai + * models](https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models.html?context=wx). + * + * Please refer to the official + * Watsonx.ai website for the most up-to-date information on available models. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ +public class WatsonxAiEmbeddingModel extends AbstractEmbeddingModel { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final WatsonxAiApi watsonxAiApi; + + /** + * Default options to be used for all embedding requests. + */ + private WatsonxAiEmbeddingOptions defaultOptions = WatsonxAiEmbeddingOptions.create() + .withModel(WatsonxAiEmbeddingOptions.DEFAULT_MODEL); + + public WatsonxAiEmbeddingModel(WatsonxAiApi watsonxAiApi) { + this.watsonxAiApi = watsonxAiApi; + } + + public WatsonxAiEmbeddingModel(WatsonxAiApi watsonxAiApi, WatsonxAiEmbeddingOptions defaultOptions) { + this.watsonxAiApi = watsonxAiApi; + this.defaultOptions = defaultOptions; + } + + @Override + public List embed(Document document) { + return embed(document.getContent()); + } + + @Override + public EmbeddingResponse call(EmbeddingRequest request) { + Assert.notEmpty(request.getInstructions(), "At least one text is required!"); + + WatsonxAiEmbeddingRequest embeddingRequest = watsonxAiEmbeddingRequest(request.getInstructions(), + request.getOptions()); + WatsonxAiEmbeddingResponse response = this.watsonxAiApi.embeddings(embeddingRequest).getBody(); + + AtomicInteger indexCounter = new AtomicInteger(0); + List embeddings = response.results() + .stream() + .map(e -> new Embedding(e.embedding(), indexCounter.getAndIncrement())) + .toList(); + + return new EmbeddingResponse(embeddings); + } + + WatsonxAiEmbeddingRequest watsonxAiEmbeddingRequest(List inputs, EmbeddingOptions options) { + + WatsonxAiEmbeddingOptions runtimeOptions = (options instanceof WatsonxAiEmbeddingOptions) + ? (WatsonxAiEmbeddingOptions) options : this.defaultOptions; + + if (!StringUtils.hasText(runtimeOptions.getModel())) { + this.logger.warn("The model cannot be null, using default model instead"); + runtimeOptions = this.defaultOptions; + } + + return WatsonxAiEmbeddingRequest.builder(inputs).withModel(runtimeOptions.getModel()).build(); + } + +} From 4b039e00b99c0ed97b4b7208c005246dcced8ef1 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:30:28 +0200 Subject: [PATCH 12/23] feat: add watsonx embedding model autoconfiguration and properties --- .../watsonxai/WatsonxAiAutoConfiguration.java | 15 +++++- .../WatsonxAiEmbeddingProperties.java | 51 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java index e640f84c779..8cb42373786 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.ai.autoconfigure.watsonxai; import org.springframework.ai.watsonx.WatsonxAiChatModel; +import org.springframework.ai.watsonx.WatsonxAiEmbeddingModel; import org.springframework.ai.watsonx.api.WatsonxAiApi; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -36,7 +37,8 @@ */ @AutoConfiguration(after = RestClientAutoConfiguration.class) @ConditionalOnClass(WatsonxAiApi.class) -@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class }) +@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class, + WatsonxAiEmbeddingProperties.class }) @ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) public class WatsonxAiAutoConfiguration { @@ -51,8 +53,19 @@ public WatsonxAiApi watsonxApi(WatsonxAiConnectionProperties properties, RestCli @Bean @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) public WatsonxAiChatModel watsonxChatModel(WatsonxAiApi watsonxApi, WatsonxAiChatProperties chatProperties) { return new WatsonxAiChatModel(watsonxApi, chatProperties.getOptions()); } + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = WatsonxAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public WatsonxAiEmbeddingModel watsonxAiEmbeddingModel(WatsonxAiApi watsonxApi, + WatsonxAiEmbeddingProperties properties) { + return new WatsonxAiEmbeddingModel(watsonxApi, properties.getOptions()); + } + } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java new file mode 100644 index 00000000000..bcf8167e809 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java @@ -0,0 +1,51 @@ +package org.springframework.ai.autoconfigure.watsonxai; + +import org.springframework.ai.watsonx.WatsonxAiEmbeddingOptions; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * Watsonx.ai Embedding autoconfiguration properties. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ +@ConfigurationProperties(WatsonxAiEmbeddingProperties.CONFIG_PREFIX) +public class WatsonxAiEmbeddingProperties { + + public static final String CONFIG_PREFIX = "spring.ai.watsonx.embedding"; + + /** + * Enable Watsonx.ai embedding model. + */ + private boolean enabled = true; + + /** + * Client lever Watsonx.ai embedding options. Use this property to configure the + * model. The null values are ignored defaulting to the defaults. + */ + @NestedConfigurationProperty + private WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create() + .withModel(WatsonxAiEmbeddingOptions.DEFAULT_MODEL); + + public String getModel() { + return this.options.getModel(); + } + + public void setModel(String model) { + this.options.setModel(model); + } + + public WatsonxAiEmbeddingOptions getOptions() { + return this.options; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return this.enabled; + } + +} From 319893f49e1fa27a6b9e2992a30a9651d3a8fbe8 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:31:42 +0200 Subject: [PATCH 13/23] fix: remove unused import --- .../org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java index 6a43c6f7d8b..4d95b83a4a3 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java @@ -4,7 +4,6 @@ import org.slf4j.LoggerFactory; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.*; -import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.watsonx.api.WatsonxAiApi; import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingRequest; import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingResponse; From 3b1b54d9f3cf4af25f5ac1205a7562d710194b01 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 03:35:59 +0200 Subject: [PATCH 14/23] fix: remove unused method --- .../springframework/ai/watsonx/api/WatsonxAiChatRequest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java index 40b0c53ab83..05dccfbdfce 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java @@ -43,11 +43,6 @@ private WatsonxAiChatRequest(String input, Map parameters, Strin this.projectId = projectId; } - public WatsonxAiChatRequest withModelId(String modelId) { - this.modelId = modelId; - return this; - } - public WatsonxAiChatRequest withProjectId(String projectId) { this.projectId = projectId; return this; From d159d573de8a63b48dbee0bec141aff8472f6f1a Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 13:57:58 +0200 Subject: [PATCH 15/23] fix: remove embedding info in watsonx chat docs --- .../main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc index 8d5837c773c..e7e8a23b775 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/watsonx-ai-chat.adoc @@ -47,7 +47,6 @@ The prefix `spring.ai.watsonx.ai` is used as the property prefix that lets you c | spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com | spring.ai.watsonx.ai.stream-endpoint | The streaming endpoint | ml/v1/text/generation_stream?version=2023-05-29 | spring.ai.watsonx.ai.text-endpoint | The text endpoint | ml/v1/text/generation?version=2023-05-29 -| spring.ai.watsonx.ai.embedding-endpoint | The embedding endpoint | ml/v1/embeddings?version=2023-05-29 | spring.ai.watsonx.ai.project-id | The project ID | - | spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - |==== From 69ec0acc4323a49a1223851b95a4e2d00c43c325 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 14:19:59 +0200 Subject: [PATCH 16/23] feat: add watsonx embedding model tests --- .../watsonx/WatsonxAiEmbeddingModelTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java diff --git a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java new file mode 100644 index 00000000000..fcc5dd00138 --- /dev/null +++ b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java @@ -0,0 +1,86 @@ +package org.springframework.ai.watsonx; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.embedding.EmbeddingOptions; +import org.springframework.ai.embedding.EmbeddingResponse; +import org.springframework.ai.watsonx.api.WatsonxAiApi; +import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingRequest; +import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingResponse; +import org.springframework.ai.watsonx.api.WatsonxAiEmbeddingResults; +import org.springframework.http.ResponseEntity; + +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class WatsonxAiEmbeddingModelTest { + + private WatsonxAiApi watsonxAiApiMock; + + private final WatsonxAiEmbeddingModel embeddingModel; + + public WatsonxAiEmbeddingModelTest() { + this.watsonxAiApiMock = mock(WatsonxAiApi.class); + this.embeddingModel = new WatsonxAiEmbeddingModel(watsonxAiApiMock); + } + + @Test + void createRequestWithOptions() { + String MODEL = "custom-model"; + List inputs = List.of("test"); + WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create().withModel(MODEL); + + WatsonxAiEmbeddingRequest request = embeddingModel.watsonxAiEmbeddingRequest(inputs, options); + + assertThat(request.getModel()).isEqualTo(MODEL); + assertThat(request.getInputs().size()).isEqualTo(inputs.size()); + } + + @Test + void createRequestWithOptionsAndInvalidModel() { + String MODEL = ""; + List inputs = List.of("test"); + WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create().withModel(MODEL); + + WatsonxAiEmbeddingRequest request = embeddingModel.watsonxAiEmbeddingRequest(inputs, options); + + assertThat(request.getModel()).isEqualTo(WatsonxAiEmbeddingOptions.DEFAULT_MODEL); + assertThat(request.getInputs().size()).isEqualTo(inputs.size()); + } + + @Test + void createRequestWithNoOptions() { + List inputs = List.of("test"); + WatsonxAiEmbeddingRequest request = embeddingModel.watsonxAiEmbeddingRequest(inputs, EmbeddingOptions.EMPTY); + + assertThat(request.getModel()).isEqualTo(WatsonxAiEmbeddingOptions.DEFAULT_MODEL); + assertThat(request.getInputs().size()).isEqualTo(inputs.size()); + } + + @Test + void singleEmbeddingWithOptions() { + List inputs = List.of("test"); + + String modelId = "mockId"; + Integer inputTokenCount = 2; + List vector = List.of(0.0D, 0.1D); + List mockResults = List.of(new WatsonxAiEmbeddingResults(vector)); + WatsonxAiEmbeddingResponse mockResponse = new WatsonxAiEmbeddingResponse(modelId, new Date(), mockResults, + inputTokenCount); + + ResponseEntity mockResponseEntity = ResponseEntity.ok(mockResponse); + when(watsonxAiApiMock.embeddings(any(WatsonxAiEmbeddingRequest.class))).thenReturn(mockResponseEntity); + + assertThat(embeddingModel).isNotNull(); + + EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World")); + assertThat(embeddingResponse.getResults()).hasSize(1); + assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty(); + assertThat(embeddingModel.dimensions()).isEqualTo(2); + } + +} From 3f5b1bd66b0956daedf953631da99f2811b1ea3e Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 14:20:18 +0200 Subject: [PATCH 17/23] feat: add watsonx embedding model docs --- .../api/embeddings/watsonx-ai-embeddings.adoc | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc new file mode 100644 index 00000000000..4be97487005 --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc @@ -0,0 +1,116 @@ += Watsonx.ai Embeddings + +With https://www.ibm.com/products/watsonx-ai[Watsonx.ai] you can run various Large Language Models (LLMs) and generate embeddings from them. +Spring AI supports the Watsonx.ai text embeddings with `WatsonxAiEmbeddingModel`. + +An embedding is a vector (list) of floating point numbers. +The distance between two vectors measures their relatedness. +Small distances suggest high relatedness and large distances suggest low relatedness. + +== Prerequisites + +You first need to have a SaaS instance of watsonx.ai (as well as an IBM Cloud account). + +Refer to https://eu-de.dataplatform.cloud.ibm.com/registration/stepone?context=wx&preselect_region=true[free-trial] to try watsonx.ai for free + +TIP: More info can be found https://www.ibm.com/products/watsonx-ai/info/trial[here] + +=== Add Repositories and BOM + +Spring AI artifacts are published in Spring Milestone and Snapshot repositories. Refer to the xref:getting-started.adoc#repositories[Repositories] section to add these repositories to your build system. + +To help with dependency management, Spring AI provides a BOM (bill of materials) to ensure that a consistent version of Spring AI is used throughout the entire project. Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build system. + + +== Auto-configuration + +Spring AI provides Spring Boot auto-configuration for the Watsonx.ai Embedding Model. +To enable it add the following dependency to your Maven `pom.xml` file: + +[source,xml] +---- + + org.springframework.ai + spring-ai-watsonx-ai-spring-boot-starter + +---- + +or to your Gradle `build.gradle` build file. + +[source,groovy] +---- +dependencies { + implementation 'org.springframework.ai:spring-ai-watsonx-ai-spring-boot-starter' +} +---- + +TIP: Refer to the xref:getting-started.adoc#dependency-management[Dependency Management] section to add the Spring AI BOM to your build file. + +The `spring.ai.watsonx.embedding.options.*` properties are used to configure the default options used for all embedding requests. + +=== Embedding Properties + +The prefix `spring.ai.watsonx.ai` is used as the property prefix that lets you connect to watsonx.ai. + +[cols="4,3,3"] +|==== +| Property | Description | Default + +| spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com +| spring.ai.watsonx.ai.embedding-endpoint | The text endpoint | ml/v1/embeddings?version=2023-05-29 +| spring.ai.watsonx.ai.project-id | The project ID | - +| spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - +|==== + +The prefix `spring.ai.watsonx.embedding.options` is the property prefix that configures the `EmbeddingModel` implementation for Watsonx.ai. + +[cols="3,5,1"] +|==== +| Property | Description | Default + +| spring.ai.watsonx.embedding.enabled | Enable Watsonx.ai embedding model | true +| spring.ai.watsonx.embedding.options.model | The embedding model to be used | ibm/slate-30m-english-rtrvr +|==== + + +== Runtime Options [[embedding-options]] + +The https://github.com/spring-projects/spring-ai/blob/main/models/spring-ai-watsonx/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingOptions.java[WatsonxAiEmbeddingOptions.java] provides the Watsonx.ai configurations, such as the model to use. + +The default options can be configured using the `spring.ai.watsonx.embedding.options` properties as well. + + +[source,java] +---- +EmbeddingResponse embeddingResponse = embeddingModel.call( + new EmbeddingRequest(List.of("Hello World", "World is big and salvation is near"), + WatsonxAiEmbeddingOptions.create() + .withModel("Different-Embedding-Model-Deployment-Name")) +); +---- + +== Sample Controller + +This will create a `EmbeddingModel` implementation that you can inject into your class. +Here is an example of a simple `@Controller` class that uses the `EmbeddingModel` implementation. + +[source,java] +---- +@RestController +public class EmbeddingController { + + private final EmbeddingModel embeddingModel; + + @Autowired + public EmbeddingController(EmbeddingModel embeddingModel) { + this.embeddingModel = embeddingModel; + } + + @GetMapping("/ai/embedding") + public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { + EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message)); + return Map.of("embedding", embeddingResponse); + } +} +---- + From 3621a8a6ddca6e948d40d6d226440f11ebf15ced Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 16:59:56 +0200 Subject: [PATCH 18/23] fix: config prefix, fix wrong path --- .../autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java index bcf8167e809..42425a265a7 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java @@ -13,7 +13,7 @@ @ConfigurationProperties(WatsonxAiEmbeddingProperties.CONFIG_PREFIX) public class WatsonxAiEmbeddingProperties { - public static final String CONFIG_PREFIX = "spring.ai.watsonx.embedding"; + public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai.embedding"; /** * Enable Watsonx.ai embedding model. From 81254f3ac1e0a5917e3b485655ab4551f3ddbedb Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Fri, 12 Jul 2024 21:06:09 +0200 Subject: [PATCH 19/23] feat: add class description and sign --- .../ai/watsonx/api/WatsonxAiChatRequest.java | 6 ++++++ .../ai/watsonx/api/WatsonxAiChatResponse.java | 6 ++++++ .../ai/watsonx/api/WatsonxAiChatResults.java | 6 ++++++ .../ai/watsonx/api/WatsonxAiEmbeddingRequest.java | 6 ++++++ .../ai/watsonx/api/WatsonxAiEmbeddingResponse.java | 6 ++++++ .../ai/watsonx/api/WatsonxAiEmbeddingResults.java | 6 ++++++ 6 files changed, 36 insertions(+) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java index 05dccfbdfce..c228372cbcd 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatRequest.java @@ -23,6 +23,12 @@ import org.springframework.ai.watsonx.WatsonxAiChatOptions; import org.springframework.util.Assert; +/** + * Java class for Watsonx.ai Chat Request object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) public class WatsonxAiChatRequest { diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java index f2db26ea1cb..36127771b35 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResponse.java @@ -22,6 +22,12 @@ import java.util.List; import java.util.Map; +/** + * Java class for Watsonx.ai Chat Response object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) public record WatsonxAiChatResponse( diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java index ef21689180d..316f8c2e478 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiChatResults.java @@ -18,6 +18,12 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Java class for Watsonx.ai Chat Results object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ // @formatter:off @JsonInclude(JsonInclude.Include.NON_NULL) public record WatsonxAiChatResults( diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java index 08b0f83cf7c..331dfa0a1af 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingRequest.java @@ -6,6 +6,12 @@ import java.util.List; +/** + * Java class for Watsonx.ai Embedding Request object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ @JsonInclude(JsonInclude.Include.NON_NULL) public class WatsonxAiEmbeddingRequest { diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java index 1b8c77030d8..ec1ae022605 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResponse.java @@ -6,6 +6,12 @@ import java.util.Date; import java.util.List; +/** + * Java class for Watsonx.ai Embedding Response object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ @JsonInclude(JsonInclude.Include.NON_NULL) public record WatsonxAiEmbeddingResponse(@JsonProperty("model_id") String model, @JsonProperty("created_at") Date createdAt, @JsonProperty("results") List results, diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java index 362b117c6a6..df715a466f8 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java @@ -5,6 +5,12 @@ import java.util.List; +/** + * Java class for Watsonx.ai Embedding Results object. + * + * @author Pablo Sanchidrian Herrera + * @since 1.0.0 + */ @JsonInclude(JsonInclude.Include.NON_NULL) public record WatsonxAiEmbeddingResults(@JsonProperty("embedding") List embedding) { } From 666822fe52cb46490423391f4d568d3858c41a05 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 13 Jul 2024 14:14:29 +0200 Subject: [PATCH 20/23] fix: issue with property name, 'ai' missing --- .../ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc index 4be97487005..0c431fbbae9 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc @@ -68,8 +68,8 @@ The prefix `spring.ai.watsonx.embedding.options` is the property prefix that con |==== | Property | Description | Default -| spring.ai.watsonx.embedding.enabled | Enable Watsonx.ai embedding model | true -| spring.ai.watsonx.embedding.options.model | The embedding model to be used | ibm/slate-30m-english-rtrvr +| spring.ai.watsonx.ai.embedding.enabled | Enable Watsonx.ai embedding model | true +| spring.ai.watsonx.ai.embedding.options.model | The embedding model to be used | ibm/slate-30m-english-rtrvr |==== From 3a34653e1b04ae25ba0a72f407e2e0dbc0e920ef Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 13 Jul 2024 14:18:32 +0200 Subject: [PATCH 21/23] fix: update embedding controller example --- .../ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc index 0c431fbbae9..225a5fdf708 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc @@ -107,10 +107,9 @@ public class EmbeddingController { } @GetMapping("/ai/embedding") - public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { - EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message)); - return Map.of("embedding", embeddingResponse); + public ResponseEntity embedding(@RequestParam String text) { + EmbeddingResponse response = this.embedding.embedForResponse(List.of(text)); + return ResponseEntity.ok(response.getResult()); } } ---- - From 280b16b46a35f882376d5eb65a2f3c1175e20eb0 Mon Sep 17 00:00:00 2001 From: PabloSanchi Date: Sat, 13 Jul 2024 14:19:42 +0200 Subject: [PATCH 22/23] fix: wrong default embedding endpoint --- .../ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc | 2 +- .../autoconfigure/watsonxai/WatsonxAiConnectionProperties.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc index 225a5fdf708..fbdc8c53ad3 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/watsonx-ai-embeddings.adoc @@ -57,7 +57,7 @@ The prefix `spring.ai.watsonx.ai` is used as the property prefix that lets you c | Property | Description | Default | spring.ai.watsonx.ai.base-url | The URL to connect to | https://us-south.ml.cloud.ibm.com -| spring.ai.watsonx.ai.embedding-endpoint | The text endpoint | ml/v1/embeddings?version=2023-05-29 +| spring.ai.watsonx.ai.embedding-endpoint | The text endpoint | ml/v1/text/embeddings?version=2023-05-29 | spring.ai.watsonx.ai.project-id | The project ID | - | spring.ai.watsonx.ai.iam-token | The IBM Cloud account IAM token | - |==== diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java index 62c8ea07b99..0ffc3656d18 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java @@ -35,7 +35,7 @@ public class WatsonxAiConnectionProperties { private String textEndpoint = "ml/v1/text/generation?version=2023-05-29"; - private String embeddingEndpoint = "ml/v1/embeddings?version=2023-05-29"; + private String embeddingEndpoint = "ml/v1/text/embeddings?version=2023-05-29"; private String projectId; From 1ffdfa3ad43904150e32ac810b5472cb46a3d15c Mon Sep 17 00:00:00 2001 From: Pablo Sanchidrian Date: Sat, 24 Aug 2024 02:15:52 +0200 Subject: [PATCH 23/23] fix: update code to met interfaces requirements --- .../ai/watsonx/WatsonxAiEmbeddingModel.java | 2 +- .../ai/watsonx/WatsonxAiEmbeddingOptions.java | 7 +++++++ .../ai/watsonx/api/WatsonxAiEmbeddingResults.java | 2 +- .../ai/watsonx/WatsonxAiEmbeddingModelTest.java | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java index 4d95b83a4a3..00aaa1d0985 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModel.java @@ -48,7 +48,7 @@ public WatsonxAiEmbeddingModel(WatsonxAiApi watsonxAiApi, WatsonxAiEmbeddingOpti } @Override - public List embed(Document document) { + public float[] embed(Document document) { return embed(document.getContent()); } diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java index 71d7ce698b3..9db6b6dd517 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingOptions.java @@ -1,5 +1,6 @@ package org.springframework.ai.watsonx; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import org.springframework.ai.embedding.EmbeddingOptions; @@ -34,6 +35,12 @@ public void setModel(String model) { this.model = model; } + @Override + @JsonIgnore + public Integer getDimensions() { + return null; + } + /** * Helper factory method to create a new {@link WatsonxAiEmbeddingOptions} instance. * @return A new {@link WatsonxAiEmbeddingOptions} instance. diff --git a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java index df715a466f8..975a1195e9e 100644 --- a/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java +++ b/models/spring-ai-watsonx-ai/src/main/java/org/springframework/ai/watsonx/api/WatsonxAiEmbeddingResults.java @@ -12,5 +12,5 @@ * @since 1.0.0 */ @JsonInclude(JsonInclude.Include.NON_NULL) -public record WatsonxAiEmbeddingResults(@JsonProperty("embedding") List embedding) { +public record WatsonxAiEmbeddingResults(@JsonProperty("embedding") float[] embedding) { } diff --git a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java index fcc5dd00138..42e6c0cc5bf 100644 --- a/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java +++ b/models/spring-ai-watsonx-ai/src/test/java/org/springframework/ai/watsonx/WatsonxAiEmbeddingModelTest.java @@ -67,7 +67,7 @@ void singleEmbeddingWithOptions() { String modelId = "mockId"; Integer inputTokenCount = 2; - List vector = List.of(0.0D, 0.1D); + float[] vector = new float[] { 1.0f, 2.0f }; List mockResults = List.of(new WatsonxAiEmbeddingResults(vector)); WatsonxAiEmbeddingResponse mockResponse = new WatsonxAiEmbeddingResponse(modelId, new Date(), mockResults, inputTokenCount);