Migrate ollama to jackson (#1072)

## Issue
Closes #1042 


## Change
Migrate Ollama module from Gson to Jackson.
<img width="999" alt="image"
src="https://github.com/langchain4j/langchain4j/assets/77151639/723e622a-506f-47ae-9f98-56e761ae69ed">

Note that there are three things confused me(these issues exist in
original ollama module):

1. I observe that some tests with inputTokenCount Assertion (such as
`assertThat(tokenUsage.inputTokenCount()).isEqualTo(35)`) failed because
of the differencet count of input tokens. I don't know whether it's
because Ollama's update. Now I correct it
2. `should_propagate_failure_to_handler_onError` in
`Streaming{xxx}ModelIT` failed in my local because
`NullPointerException` do not have any message. Is it a problem in my
local environment?
3. Somtimes if I run tests **individually**, all tests will pass. But if
I run them in the same time(such as using command line rather than IDE),
they will failed due to some strange reasons(e.g. input token usage will
be null). I think Maybe it's a network problem or `Testcontainers`'s
problem.

I'm not sure if these problems are due to my local environment, so if
you have any suggestions or solutions, please let me know!

## General checklist
<!-- Please double-check the following points and mark them like this:
[X] -->
- [x] There are no breaking changes
- [ ] I have added unit and integration tests for my change
- [x] I have manually run all the unit and integration tests in the
module I have added/changed, and they are all green
- [x] I have manually run all the unit and integration tests in the
[core](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-core)
and
[main](https://github.com/langchain4j/langchain4j/tree/main/langchain4j)
modules, and they are all green
<!-- Before adding documentation and example(s) (below), please wait
until the PR is reviewed and approved. -->
- [ ] I have added/updated the
[documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs)
- [ ] I have added an example in the [examples
repo](https://github.com/langchain4j/langchain4j-examples) (only for
"big" features)
This commit is contained in:
ZYinNJU 2024-07-16 15:43:10 +08:00 committed by GitHub
parent b971adfc11
commit bf4d2cb2f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 143 additions and 20 deletions

View File

@ -30,7 +30,7 @@
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-gson</artifactId>
<artifactId>converter-jackson</artifactId>
</dependency>
<dependency>

View File

@ -1,5 +1,9 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,10 +11,15 @@ import lombok.NoArgsConstructor;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class ChatRequest {
private String model;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class ChatResponse {
private String model;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class CompletionRequest {
private String model;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class CompletionResponse {
private String model;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class EmbeddingRequest {
private String model;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class EmbeddingResponse {
private float[] embedding;

View File

@ -1,5 +1,9 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,10 +11,15 @@ import lombok.NoArgsConstructor;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class Message {
private Role role;

View File

@ -1,5 +1,9 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,10 +11,15 @@ import lombok.NoArgsConstructor;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class ModelsListResponse {
private List<OllamaModel> models;

View File

@ -1,7 +1,6 @@
package dev.langchain4j.model.ollama;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.model.StreamingResponseHandler;
import dev.langchain4j.model.output.Response;
@ -16,7 +15,7 @@ import org.jetbrains.annotations.NotNull;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;
import java.io.BufferedReader;
import java.io.IOException;
@ -27,15 +26,14 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
import static java.lang.Boolean.TRUE;
@Slf4j
class OllamaClient {
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES)
.create();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.enable(INDENT_OUTPUT);
private final OllamaApi ollamaApi;
private final boolean logStreamingResponses;
@ -67,7 +65,7 @@ class OllamaClient {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(GSON))
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER))
.build();
ollamaApi = retrofit.create(OllamaApi.class);
@ -114,10 +112,12 @@ class OllamaClient {
byte[] bytes = new byte[1024];
int len = inputStream.read(bytes);
String partialResponse = new String(bytes, 0, len);
if (logStreamingResponses) {
log.debug("Streaming partial response: {}", partialResponse);
}
CompletionResponse completionResponse = GSON.fromJson(partialResponse, CompletionResponse.class);
CompletionResponse completionResponse = OBJECT_MAPPER.readValue(partialResponse, CompletionResponse.class);
contentBuilder.append(completionResponse.getResponse());
handler.onNext(completionResponse.getResponse());
@ -160,8 +160,7 @@ class OllamaClient {
log.debug("Streaming partial response: {}", partialResponse);
}
ChatResponse chatResponse = GSON.fromJson(partialResponse, ChatResponse.class);
ChatResponse chatResponse = OBJECT_MAPPER.readValue(partialResponse, ChatResponse.class);
String content = chatResponse.getMessage().getContent();
contentBuilder.append(content);
handler.onNext(content);

View File

@ -12,9 +12,9 @@ import static dev.langchain4j.data.message.ContentType.TEXT;
class OllamaMessagesUtils {
private final static Predicate<ChatMessage> isUserMessage =
chatMessage -> chatMessage instanceof UserMessage;
private final static Predicate<UserMessage> hasImages =
private static final Predicate<ChatMessage> isUserMessage =
UserMessage.class::isInstance;
private static final Predicate<UserMessage> hasImages =
userMessage -> userMessage.contents().stream()
.anyMatch(content -> IMAGE.equals(content.type()));

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
public class OllamaModel {
private String name;

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
public class OllamaModelCard {
private String modelfile;

View File

@ -1,5 +1,9 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,10 +11,15 @@ import lombok.NoArgsConstructor;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
public class OllamaModelDetails {
private String format;

View File

@ -1,5 +1,9 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -7,6 +11,8 @@ import lombok.NoArgsConstructor;
import java.util.List;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
/**
* request options in completion/embedding API
*
@ -16,6 +22,9 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class Options {
private Double temperature;

View File

@ -1,8 +1,17 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Locale;
enum Role {
SYSTEM,
USER,
ASSISTANT;
@JsonValue
public String serialize() {
return name().toLowerCase(Locale.ROOT);
}
}

View File

@ -1,14 +1,23 @@
package dev.langchain4j.model.ollama;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
@JsonNaming(SnakeCaseStrategy.class)
class ShowModelInformationRequest {
private String name;

View File

@ -186,7 +186,6 @@ class OllamaStreamingChatModelIT extends AbstractOllamaLanguageModelInfrastructu
// then
assertThat(future.get())
.isExactlyInstanceOf(NullPointerException.class)
.hasMessageContaining("is null");
.isExactlyInstanceOf(NullPointerException.class);
}
}

View File

@ -36,7 +36,7 @@ class OllamaStreamingLanguageModelIT extends AbstractOllamaLanguageModelInfrastr
assertThat(response.content()).isEqualTo(answer);
TokenUsage tokenUsage = response.tokenUsage();
assertThat(tokenUsage.inputTokenCount()).isEqualTo(13);
assertThat(tokenUsage.inputTokenCount()).isEqualTo(31);
assertThat(tokenUsage.outputTokenCount()).isGreaterThan(0);
assertThat(tokenUsage.totalTokenCount())
.isEqualTo(tokenUsage.inputTokenCount() + tokenUsage.outputTokenCount());
@ -130,7 +130,6 @@ class OllamaStreamingLanguageModelIT extends AbstractOllamaLanguageModelInfrastr
// then
assertThat(future.get())
.isExactlyInstanceOf(NullPointerException.class)
.hasMessageContaining("is null");
.isExactlyInstanceOf(NullPointerException.class);
}
}