Refactoring of ChatClient to add fluent API and introduce Model as dependent object

* Rename the ModelClient class hierarchy into Model:
  - Rename ModelClient into Model. Update all code and doc references.
  - Rename ChatClient to ChatModel. Update all ChatClient suffixes and chatClient fields and variables in code and doc.
  - Rename EmbeddingClient into EmbeddingModel. Update the XxxEmbeddingClient class and variable suffixes and embeddingClient variables and fields in code and docs.
  - Rename ImageClient into ImageModel.
  - Rename SpeechClient into SpeechModel.
  - Rename TranscriptionClient into TranscriptionModel.
  - Update all javadocs and antora pages. Update the related diagrams.

* Create fluent API in ChatClient interface that now includes streaming support
* Add OpenAI FunctionCallbackWrapper2IT auto-config tests.
* Add ChatClientTest mockito testing.
* Add ChatModel#getDefaultOptions(), and remove @FunctionalInterface

* ChatModel enums extend the new ModelDescription interface.
* Implement fromOptions copy method in every ChatOptions implementation.
* Extend ChatClient to use the model default options if not provided explicitly.

* Update readme to provide guidance on how to adapt to breaking changes.

Co-authored-by: Christian Tzolov <ctzolov@vmware.com>
Co-authored-by: Mark Pollack <mpollack@vmware.com>
This commit is contained in:
Josh Long
2024-05-18 21:30:51 +02:00
committed by Mark Pollack
parent bce45c2d2f
commit fbfc87e814
426 changed files with 4815 additions and 2912 deletions

View File

@@ -24,7 +24,7 @@ import java.util.Map;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.AbstractEmbeddingClient;
import org.springframework.ai.embedding.AbstractEmbeddingModel;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.embedding.EmbeddingRequest;
@@ -39,12 +39,12 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* <a href="https://postgresml.org">PostgresML</a> EmbeddingClient
* <a href="https://postgresml.org">PostgresML</a> EmbeddingModel
*
* @author Toshiaki Maki
* @author Christian Tzolov
*/
public class PostgresMlEmbeddingClient extends AbstractEmbeddingClient implements InitializingBean {
public class PostgresMlEmbeddingModel extends AbstractEmbeddingModel implements InitializingBean {
public static final String DEFAULT_TRANSFORMER_MODEL = "distilbert-base-uncased";
@@ -83,16 +83,16 @@ public class PostgresMlEmbeddingClient extends AbstractEmbeddingClient implement
* a constructor
* @param jdbcTemplate JdbcTemplate
*/
public PostgresMlEmbeddingClient(JdbcTemplate jdbcTemplate) {
public PostgresMlEmbeddingModel(JdbcTemplate jdbcTemplate) {
this(jdbcTemplate, PostgresMlEmbeddingOptions.builder().build());
}
/**
* a PostgresMlEmbeddingClient constructor
* a PostgresMlEmbeddingModel constructor
* @param jdbcTemplate JdbcTemplate to use to interact with the database.
* @param options PostgresMlEmbeddingOptions to configure the client.
*/
public PostgresMlEmbeddingClient(JdbcTemplate jdbcTemplate, PostgresMlEmbeddingOptions options) {
public PostgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, PostgresMlEmbeddingOptions options) {
Assert.notNull(jdbcTemplate, "jdbc template must not be null.");
Assert.notNull(options, "options must not be null.");
Assert.notNull(options.getTransformer(), "transformer must not be null.");
@@ -110,7 +110,7 @@ public class PostgresMlEmbeddingClient extends AbstractEmbeddingClient implement
* @param transformer huggingface sentence-transformer name
*/
@Deprecated(since = "0.8.0", forRemoval = true)
public PostgresMlEmbeddingClient(JdbcTemplate jdbcTemplate, String transformer) {
public PostgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, String transformer) {
this(jdbcTemplate, transformer, VectorType.PG_ARRAY);
}
@@ -122,7 +122,7 @@ public class PostgresMlEmbeddingClient extends AbstractEmbeddingClient implement
* @param vectorType vector type in PostgreSQL
*/
@Deprecated(since = "0.8.0", forRemoval = true)
public PostgresMlEmbeddingClient(JdbcTemplate jdbcTemplate, String transformer, VectorType vectorType) {
public PostgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, String transformer, VectorType vectorType) {
this(jdbcTemplate, transformer, vectorType, Map.of(), MetadataMode.EMBED);
}
@@ -135,7 +135,7 @@ public class PostgresMlEmbeddingClient extends AbstractEmbeddingClient implement
* @param kwargs optional arguments
*/
@Deprecated(since = "0.8.0", forRemoval = true)
public PostgresMlEmbeddingClient(JdbcTemplate jdbcTemplate, String transformer, VectorType vectorType,
public PostgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, String transformer, VectorType vectorType,
Map<String, Object> kwargs, MetadataMode metadataMode) {
Assert.notNull(jdbcTemplate, "jdbc template must not be null.");
Assert.notNull(transformer, "transformer must not be null.");

View File

@@ -24,7 +24,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.postgresml.PostgresMlEmbeddingClient.VectorType;
import org.springframework.ai.postgresml.PostgresMlEmbeddingModel.VectorType;
/**
* @author Christian Tzolov
@@ -36,7 +36,7 @@ public class PostgresMlEmbeddingOptions implements EmbeddingOptions {
/**
* The Huggingface transformer model to use for the embedding.
*/
private @JsonProperty("transformer") String transformer = PostgresMlEmbeddingClient.DEFAULT_TRANSFORMER_MODEL;
private @JsonProperty("transformer") String transformer = PostgresMlEmbeddingModel.DEFAULT_TRANSFORMER_MODEL;
/**
* PostgresML vector type to use for the embedding.

View File

@@ -30,7 +30,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.ai.embedding.EmbeddingOptions;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.postgresml.PostgresMlEmbeddingClient.VectorType;
import org.springframework.ai.postgresml.PostgresMlEmbeddingModel.VectorType;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
@@ -56,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
@Disabled("Disabled from automatic execution, as it requires an excessive amount of memory (over 9GB)!")
class PostgresMlEmbeddingClientIT {
class PostgresMlEmbeddingModelIT {
@Container
@ServiceConnection
@@ -80,51 +80,51 @@ class PostgresMlEmbeddingClientIT {
@Test
void embed() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate);
embeddingClient.afterPropertiesSet();
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate);
embeddingModel.afterPropertiesSet();
List<Double> embed = embeddingClient.embed("Hello World!");
List<Double> embed = embeddingModel.embed("Hello World!");
assertThat(embed).hasSize(768);
}
@Test
void embedWithPgVector() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate,
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate,
PostgresMlEmbeddingOptions.builder()
.withTransformer("distilbert-base-uncased")
.withVectorType(PostgresMlEmbeddingClient.VectorType.PG_VECTOR)
.withVectorType(PostgresMlEmbeddingModel.VectorType.PG_VECTOR)
.build());
embeddingClient.afterPropertiesSet();
embeddingModel.afterPropertiesSet();
List<Double> embed = embeddingClient.embed(new Document("Hello World!"));
List<Double> embed = embeddingModel.embed(new Document("Hello World!"));
assertThat(embed).hasSize(768);
}
@Test
void embedWithDifferentModel() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate,
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate,
PostgresMlEmbeddingOptions.builder().withTransformer("intfloat/e5-small").build());
embeddingClient.afterPropertiesSet();
embeddingModel.afterPropertiesSet();
List<Double> embed = embeddingClient.embed(new Document("Hello World!"));
List<Double> embed = embeddingModel.embed(new Document("Hello World!"));
assertThat(embed).hasSize(384);
}
@Test
void embedWithKwargs() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate,
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate,
PostgresMlEmbeddingOptions.builder()
.withTransformer("distilbert-base-uncased")
.withVectorType(PostgresMlEmbeddingClient.VectorType.PG_ARRAY)
.withVectorType(PostgresMlEmbeddingModel.VectorType.PG_ARRAY)
.withKwargs(Map.of("device", "cpu"))
.withMetadataMode(MetadataMode.EMBED)
.build());
embeddingClient.afterPropertiesSet();
embeddingModel.afterPropertiesSet();
List<Double> embed = embeddingClient.embed(new Document("Hello World!"));
List<Double> embed = embeddingModel.embed(new Document("Hello World!"));
assertThat(embed).hasSize(768);
}
@@ -132,14 +132,14 @@ class PostgresMlEmbeddingClientIT {
@ParameterizedTest
@ValueSource(strings = { "PG_ARRAY", "PG_VECTOR" })
void embedForResponse(String vectorType) {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate,
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate,
PostgresMlEmbeddingOptions.builder()
.withTransformer("distilbert-base-uncased")
.withVectorType(VectorType.valueOf(vectorType))
.build());
embeddingClient.afterPropertiesSet();
embeddingModel.afterPropertiesSet();
EmbeddingResponse embeddingResponse = embeddingClient
EmbeddingResponse embeddingResponse = embeddingModel
.embedForResponse(List.of("Hello World!", "Spring AI!", "LLM!"));
assertThat(embeddingResponse).isNotNull();
@@ -157,16 +157,16 @@ class PostgresMlEmbeddingClientIT {
@Test
void embedCallWithRequestOptionsOverride() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate,
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate,
PostgresMlEmbeddingOptions.builder()
.withTransformer("distilbert-base-uncased")
.withVectorType(VectorType.PG_VECTOR)
.build());
embeddingClient.afterPropertiesSet();
embeddingModel.afterPropertiesSet();
var request1 = new EmbeddingRequest(List.of("Hello World!", "Spring AI!", "LLM!"), EmbeddingOptions.EMPTY);
EmbeddingResponse embeddingResponse = embeddingClient.call(request1);
EmbeddingResponse embeddingResponse = embeddingModel.call(request1);
assertThat(embeddingResponse).isNotNull();
assertThat(embeddingResponse.getResults()).hasSize(3);
@@ -188,7 +188,7 @@ class PostgresMlEmbeddingClientIT {
.withKwargs(Map.of("device", "cpu"))
.build());
embeddingResponse = embeddingClient.call(request2);
embeddingResponse = embeddingModel.call(request2);
assertThat(embeddingResponse).isNotNull();
assertThat(embeddingResponse.getResults()).hasSize(3);
@@ -205,11 +205,11 @@ class PostgresMlEmbeddingClientIT {
@Test
void dimensions() {
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(this.jdbcTemplate);
embeddingClient.afterPropertiesSet();
Assertions.assertThat(embeddingClient.dimensions()).isEqualTo(768);
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(this.jdbcTemplate);
embeddingModel.afterPropertiesSet();
Assertions.assertThat(embeddingModel.dimensions()).isEqualTo(768);
// cached
Assertions.assertThat(embeddingClient.dimensions()).isEqualTo(768);
Assertions.assertThat(embeddingModel.dimensions()).isEqualTo(768);
}
@SpringBootApplication

View File

@@ -34,8 +34,8 @@ public class PostgresMlEmbeddingOptionsTests {
public void defaultOptions() {
PostgresMlEmbeddingOptions options = PostgresMlEmbeddingOptions.builder().build();
assertThat(options.getTransformer()).isEqualTo(PostgresMlEmbeddingClient.DEFAULT_TRANSFORMER_MODEL);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingClient.VectorType.PG_ARRAY);
assertThat(options.getTransformer()).isEqualTo(PostgresMlEmbeddingModel.DEFAULT_TRANSFORMER_MODEL);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_ARRAY);
assertThat(options.getKwargs()).isEqualTo(Map.of());
assertThat(options.getMetadataMode()).isEqualTo(org.springframework.ai.document.MetadataMode.EMBED);
}
@@ -44,13 +44,13 @@ public class PostgresMlEmbeddingOptionsTests {
public void newOptions() {
PostgresMlEmbeddingOptions options = PostgresMlEmbeddingOptions.builder()
.withTransformer("intfloat/e5-small")
.withVectorType(PostgresMlEmbeddingClient.VectorType.PG_VECTOR)
.withVectorType(PostgresMlEmbeddingModel.VectorType.PG_VECTOR)
.withMetadataMode(org.springframework.ai.document.MetadataMode.ALL)
.withKwargs(Map.of("device", "cpu"))
.build();
assertThat(options.getTransformer()).isEqualTo("intfloat/e5-small");
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingClient.VectorType.PG_VECTOR);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_VECTOR);
assertThat(options.getKwargs()).isEqualTo(Map.of("device", "cpu"));
assertThat(options.getMetadataMode()).isEqualTo(org.springframework.ai.document.MetadataMode.ALL);
}
@@ -59,37 +59,37 @@ public class PostgresMlEmbeddingOptionsTests {
public void mergeOptions() {
var jdbcTemplate = Mockito.mock(JdbcTemplate.class);
PostgresMlEmbeddingClient embeddingClient = new PostgresMlEmbeddingClient(jdbcTemplate);
PostgresMlEmbeddingModel embeddingModel = new PostgresMlEmbeddingModel(jdbcTemplate);
PostgresMlEmbeddingOptions options = embeddingClient.mergeOptions(EmbeddingOptions.EMPTY);
PostgresMlEmbeddingOptions options = embeddingModel.mergeOptions(EmbeddingOptions.EMPTY);
// Default options
assertThat(options.getTransformer()).isEqualTo(PostgresMlEmbeddingClient.DEFAULT_TRANSFORMER_MODEL);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingClient.VectorType.PG_ARRAY);
assertThat(options.getTransformer()).isEqualTo(PostgresMlEmbeddingModel.DEFAULT_TRANSFORMER_MODEL);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_ARRAY);
assertThat(options.getKwargs()).isEqualTo(Map.of());
assertThat(options.getMetadataMode()).isEqualTo(org.springframework.ai.document.MetadataMode.EMBED);
// Partial override
options = embeddingClient.mergeOptions(PostgresMlEmbeddingOptions.builder()
options = embeddingModel.mergeOptions(PostgresMlEmbeddingOptions.builder()
.withTransformer("intfloat/e5-small")
.withKwargs(Map.of("device", "cpu"))
.build());
assertThat(options.getTransformer()).isEqualTo("intfloat/e5-small");
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingClient.VectorType.PG_ARRAY); // Default
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_ARRAY); // Default
assertThat(options.getKwargs()).isEqualTo(Map.of("device", "cpu"));
assertThat(options.getMetadataMode()).isEqualTo(org.springframework.ai.document.MetadataMode.EMBED); // Default
// Complete override
options = embeddingClient.mergeOptions(PostgresMlEmbeddingOptions.builder()
options = embeddingModel.mergeOptions(PostgresMlEmbeddingOptions.builder()
.withTransformer("intfloat/e5-small")
.withVectorType(PostgresMlEmbeddingClient.VectorType.PG_VECTOR)
.withVectorType(PostgresMlEmbeddingModel.VectorType.PG_VECTOR)
.withMetadataMode(org.springframework.ai.document.MetadataMode.ALL)
.withKwargs(Map.of("device", "cpu"))
.build());
assertThat(options.getTransformer()).isEqualTo("intfloat/e5-small");
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingClient.VectorType.PG_VECTOR);
assertThat(options.getVectorType()).isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_VECTOR);
assertThat(options.getKwargs()).isEqualTo(Map.of("device", "cpu"));
assertThat(options.getMetadataMode()).isEqualTo(org.springframework.ai.document.MetadataMode.ALL);
}