Skip to content

Commit 4e06a70

Browse files
committed
fix: Use upstream mixins for codec
1 parent 69560bb commit 4e06a70

File tree

2 files changed

+100
-2
lines changed

2 files changed

+100
-2
lines changed

core/runtime/src/main/java/io/quarkiverse/langchain4j/QuarkusJsonCodecFactory.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
import com.fasterxml.jackson.annotation.JsonAutoDetect;
1313
import com.fasterxml.jackson.annotation.JsonInclude;
14+
import com.fasterxml.jackson.annotation.JsonProperty;
15+
import com.fasterxml.jackson.annotation.JsonSubTypes;
16+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
1417
import com.fasterxml.jackson.annotation.PropertyAccessor;
1518
import com.fasterxml.jackson.core.JsonParseException;
1619
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -21,9 +24,17 @@
2124
import com.fasterxml.jackson.databind.ObjectWriter;
2225
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
2326
import com.fasterxml.jackson.databind.SerializationFeature;
27+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2428
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
2529
import com.fasterxml.jackson.databind.module.SimpleModule;
2630

31+
import dev.langchain4j.data.message.AiMessage;
32+
import dev.langchain4j.data.message.ChatMessage;
33+
import dev.langchain4j.data.message.ChatMessageType;
34+
import dev.langchain4j.data.message.CustomMessage;
35+
import dev.langchain4j.data.message.SystemMessage;
36+
import dev.langchain4j.data.message.ToolExecutionResultMessage;
37+
import dev.langchain4j.data.message.UserMessage;
2738
import dev.langchain4j.internal.Json;
2839
import dev.langchain4j.spi.json.JsonCodecFactory;
2940
import io.quarkiverse.langchain4j.runtime.jackson.CustomLocalDateDeserializer;
@@ -104,16 +115,69 @@ public static class ObjectMapperHolder {
104115
public static final ObjectWriter WRITER;
105116

106117
static {
118+
// Start with Arc container ObjectMapper to preserve Quarkus integration
107119
MAPPER = Arc.container().instance(ObjectMapper.class).get()
108120
.copy()
109121
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
110122
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
111-
.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)
112-
.registerModule(SnakeCaseObjectMapperHolder.QuarkusLangChain4jModule.INSTANCE);
123+
.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
124+
125+
// Add chat message mixins to preserve thinking field deserialization
126+
MAPPER.addMixIn(ChatMessage.class, ChatMessageMixin.class);
127+
MAPPER.addMixIn(AiMessage.class, AiMessageMixin.class);
128+
MAPPER.addMixIn(UserMessage.class, UserMessageMixin.class);
129+
MAPPER.addMixIn(SystemMessage.class, SystemMessageMixin.class);
130+
MAPPER.addMixIn(ToolExecutionResultMessage.class, ToolExecutionResultMessageMixin.class);
131+
MAPPER.addMixIn(CustomMessage.class, CustomMessageMixin.class);
132+
133+
// Register Quarkus-specific module
134+
MAPPER.registerModule(SnakeCaseObjectMapperHolder.QuarkusLangChain4jModule.INSTANCE);
135+
113136
WRITER = MAPPER.writerWithDefaultPrettyPrinter();
114137
}
115138
}
116139

140+
/**
141+
* Jackson mixins for chat message deserialization.
142+
* These enable proper deserialization of chat messages including the thinking field in AiMessage.
143+
* Based on mixins from dev.langchain4j.data.message.JacksonChatMessageJsonCodec.
144+
*/
145+
@JsonInclude(JsonInclude.Include.NON_NULL)
146+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
147+
@JsonSubTypes({
148+
@JsonSubTypes.Type(value = SystemMessage.class, name = "SYSTEM"),
149+
@JsonSubTypes.Type(value = UserMessage.class, name = "USER"),
150+
@JsonSubTypes.Type(value = AiMessage.class, name = "AI"),
151+
@JsonSubTypes.Type(value = ToolExecutionResultMessage.class, name = "TOOL_EXECUTION_RESULT"),
152+
@JsonSubTypes.Type(value = CustomMessage.class, name = "CUSTOM"),
153+
})
154+
private abstract static class ChatMessageMixin {
155+
@JsonProperty
156+
public abstract ChatMessageType type();
157+
}
158+
159+
@JsonInclude(JsonInclude.Include.NON_NULL)
160+
private abstract static class SystemMessageMixin {
161+
}
162+
163+
@JsonInclude(JsonInclude.Include.NON_NULL)
164+
@JsonDeserialize(builder = UserMessage.Builder.class)
165+
private abstract static class UserMessageMixin {
166+
}
167+
168+
@JsonInclude(JsonInclude.Include.NON_NULL)
169+
@JsonDeserialize(builder = AiMessage.Builder.class)
170+
private abstract static class AiMessageMixin {
171+
}
172+
173+
@JsonInclude(JsonInclude.Include.NON_NULL)
174+
private abstract static class ToolExecutionResultMessageMixin {
175+
}
176+
177+
@JsonInclude(JsonInclude.Include.NON_NULL)
178+
private abstract static class CustomMessageMixin {
179+
}
180+
117181
public static class SnakeCaseObjectMapperHolder {
118182
public static final ObjectMapper MAPPER = Arc.container().instance(ObjectMapper.class).get()
119183
.copy()

memory-stores/memory-store-redis/deployment/src/test/java/io/quarkiverse/langchain4j/memorystore/redis/test/RedisChatMemoryStoreTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.junit.jupiter.api.Test;
1919
import org.junit.jupiter.api.extension.RegisterExtension;
2020

21+
import dev.langchain4j.data.message.AiMessage;
2122
import dev.langchain4j.data.message.ChatMessage;
2223
import dev.langchain4j.service.MemoryId;
2324
import dev.langchain4j.service.UserMessage;
@@ -167,4 +168,37 @@ void should_keep_separate_chat_memory_for_each_user_in_store() throws IOExceptio
167168
assertThat(redisDataSource.key().exists("" + FIRST_MEMORY_ID, "" + SECOND_MEMORY_ID)).isEqualTo(0);
168169
}
169170

171+
@Test
172+
void should_persist_ai_message_thinking_field() {
173+
// assert the bean type is correct
174+
assertThat(chatMemoryStore).isInstanceOf(RedisChatMemoryStore.class);
175+
176+
// Create an AiMessage with thinking field
177+
AiMessage messageWithThinking = AiMessage.builder()
178+
.text("The answer is 42")
179+
.thinking("Let me reason through this carefully. The question asks about the meaning of life...")
180+
.build();
181+
182+
int memoryId = 999;
183+
184+
// Store the message
185+
chatMemoryStore.updateMessages(memoryId, List.of(messageWithThinking));
186+
187+
// Retrieve the messages
188+
List<ChatMessage> retrievedMessages = chatMemoryStore.getMessages(memoryId);
189+
190+
// Assert the message was retrieved and the thinking field is preserved
191+
assertThat(retrievedMessages).hasSize(1);
192+
ChatMessage retrievedMessage = retrievedMessages.get(0);
193+
assertThat(retrievedMessage).isInstanceOf(AiMessage.class);
194+
195+
AiMessage retrievedAiMessage = (AiMessage) retrievedMessage;
196+
assertThat(retrievedAiMessage.text()).isEqualTo("The answer is 42");
197+
assertThat(retrievedAiMessage.thinking()).isEqualTo(
198+
"Let me reason through this carefully. The question asks about the meaning of life...");
199+
200+
// Clean up
201+
chatMemoryStore.deleteMessages(memoryId);
202+
}
203+
170204
}

0 commit comments

Comments
 (0)