Skip to content

Commit aba837a

Browse files
tzolovilayaperumalg
authored andcommitted
feat(chroma): improve handling and testing of complex metadata values
- Convert non-primitive metadata values to JSON strings in ChromaApi.AddEmbeddingsRequest for compatibility - Add tests to verify metadata conversion and complex metadata handling in Chroma vector store integration - Ensure OpenAiChatModel always returns annotations as a list of maps in metadata - Add test dependency on spring-ai-advisors-vector-store for advisor-related tests - Add json-unit-assertj for improved JSON assertions and validating JSON content in tests Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
1 parent f6d73a9 commit aba837a

File tree

7 files changed

+97
-2
lines changed

7 files changed

+97
-2
lines changed

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,12 @@
103103
<version>${project.parent.version}</version>
104104
<scope>test</scope>
105105
</dependency>
106+
107+
<dependency>
108+
<groupId>org.springframework.ai</groupId>
109+
<artifactId>spring-ai-advisors-vector-store</artifactId>
110+
<version>${project.parent.version}</version>
111+
<scope>test</scope>
112+
</dependency>
106113
</dependencies>
107114
</project>

auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-chroma/src/test/java/org/springframework/ai/vectorstore/chroma/autoconfigure/ChromaVectorStoreAutoConfigurationIT.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
import org.testcontainers.junit.jupiter.Container;
2929
import org.testcontainers.junit.jupiter.Testcontainers;
3030

31+
import org.springframework.ai.chat.client.ChatClientRequest;
32+
import org.springframework.ai.chat.client.ChatClientResponse;
33+
import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor;
34+
import org.springframework.ai.chat.messages.AssistantMessage;
35+
import org.springframework.ai.chat.model.ChatResponse;
36+
import org.springframework.ai.chat.model.Generation;
37+
import org.springframework.ai.chat.prompt.Prompt;
3138
import org.springframework.ai.chroma.vectorstore.ChromaVectorStore;
3239
import org.springframework.ai.document.Document;
3340
import org.springframework.ai.embedding.EmbeddingModel;
@@ -68,6 +75,39 @@ public class ChromaVectorStoreAutoConfigurationIT {
6875
"spring.ai.vectorstore.chroma.client.port=" + chroma.getMappedPort(8000),
6976
"spring.ai.vectorstore.chroma.collectionName=TestCollection");
7077

78+
@Test
79+
public void verifyThatChromaCanHandleComplexMetadataValues() {
80+
this.contextRunner.withPropertyValues("spring.ai.vectorstore.chroma.initializeSchema=true").run(context -> {
81+
82+
VectorStore vectorStore = context.getBean(VectorStore.class);
83+
84+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
85+
.defaultTopK(5)
86+
.build();
87+
88+
assertThat(advisor.getName()).isEqualTo("VectorStoreChatMemoryAdvisor");
89+
90+
var req = ChatClientRequest.builder().prompt(Prompt.builder().content("UserPrompt").build()).build();
91+
92+
ChatClientRequest req2 = advisor.before(req, null);
93+
assertThat(req2).isNotNull();
94+
95+
var response = ChatClientResponse.builder()
96+
.chatResponse(ChatResponse.builder()
97+
.generations(List
98+
.of(new Generation(new AssistantMessage("AssistantMessage", Map.of("annotations", List.of())))))
99+
.build())
100+
.build();
101+
var res2 = advisor.after(response, null);
102+
assertThat(res2).isNotNull();
103+
104+
// Remove all documents from the store
105+
List<Document> docs = vectorStore.similaritySearch("UserPrompt, AssistantMessage");
106+
vectorStore.delete(docs.stream().map(doc -> doc.getId()).toList());
107+
108+
});
109+
}
110+
71111
@Test
72112
public void addAndSearchWithFilters() {
73113

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons
219219
"index", choice.index(),
220220
"finishReason", choice.finishReason() != null ? choice.finishReason().name() : "",
221221
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "",
222-
"annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of());
222+
"annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(Map.of()));
223223
return buildGeneration(choice, metadata, request);
224224
}).toList();
225225
// @formatter:on

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@
309309

310310
<!-- testing dependencies -->
311311
<okhttp3.version>4.12.0</okhttp3.version>
312+
<json-unit-assertj.version>4.1.0</json-unit-assertj.version>
312313

313314
<!-- MCP-->
314315
<mcp.sdk.version>0.10.0</mcp.sdk.version>

vector-stores/spring-ai-chroma-store/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@
9595
<version>${project.parent.version}</version>
9696
<scope>test</scope>
9797
</dependency>
98+
99+
<dependency>
100+
<groupId>net.javacrumbs.json-unit</groupId>
101+
<artifactId>json-unit-assertj</artifactId>
102+
<version>${json-unit-assertj.version}</version>
103+
<scope>test</scope>
104+
</dependency>
105+
98106
</dependencies>
99107

100108
</project>

vector-stores/spring-ai-chroma-store/src/main/java/org/springframework/ai/chroma/vectorstore/ChromaApi.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.ai.chroma.vectorstore.ChromaApi.QueryRequest.Include;
3232
import org.springframework.ai.chroma.vectorstore.common.ChromaApiConstants;
33+
import org.springframework.ai.util.json.JsonParser;
3334
import org.springframework.http.HttpHeaders;
3435
import org.springframework.http.MediaType;
3536
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
@@ -443,7 +444,27 @@ public record AddEmbeddingsRequest(// @formatter:off
443444
@JsonProperty("metadatas") List<Map<String, Object>> metadata,
444445
@JsonProperty("documents") List<String> documents) { // @formatter:on
445446

446-
// Convenance for adding a single embedding.
447+
public AddEmbeddingsRequest {
448+
// Process metadata to ensure all values are Integer, Boolean, or String.
449+
// Other types are converted to JSON string using JsonParser.toJson().
450+
List<Map<String, Object>> processedMetadatas = new ArrayList<>();
451+
for (Map<String, Object> meta : metadata) {
452+
Map<String, Object> processed = new HashMap<>();
453+
for (Map.Entry<String, Object> entry : meta.entrySet()) {
454+
Object value = entry.getValue();
455+
if (value instanceof Number || value instanceof Boolean || value instanceof String) {
456+
processed.put(entry.getKey(), value);
457+
}
458+
else {
459+
processed.put(entry.getKey(), JsonParser.toJson(value));
460+
}
461+
}
462+
processedMetadatas.add(processed);
463+
}
464+
metadata = processedMetadatas;
465+
}
466+
467+
// Convenience for adding a single embedding.
447468
public AddEmbeddingsRequest(String id, float[] embedding, Map<String, Object> metadata, String document) {
448469
this(List.of(id), List.of(embedding), List.of(metadata), List.of(document));
449470
}

vector-stores/spring-ai-chroma-store/src/test/java/org/springframework/ai/chroma/vectorstore/ChromaApiIT.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import static org.assertj.core.api.Assertions.assertThat;
4444
import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException;
4545
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
46+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
47+
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
4648

4749
/**
4850
* @author Christian Tzolov
@@ -275,6 +277,22 @@ void shouldFailWhenCollectionDoesNotExist() {
275277
"Collection non-existent doesn't exist and won't be created as the initializeSchema is set to false.");
276278
}
277279

280+
@Test
281+
public void testAddEmbeddingsRequestMetadataConversion() {
282+
Map<String, Object> metadata = Map.of("intVal", 42, "boolVal", true, "strVal", "hello", "doubleVal", 3.14,
283+
"listVal", List.of(1, 2, 3), "mapVal", Map.of("a", 1, "b", 2));
284+
AddEmbeddingsRequest req = new AddEmbeddingsRequest("id", new float[] { 1f, 2f, 3f }, metadata, "doc");
285+
Map<String, Object> processed = req.metadata().get(0);
286+
287+
assertThat(processed.get("intVal")).isInstanceOf(Integer.class);
288+
assertThat(processed.get("boolVal")).isInstanceOf(Boolean.class);
289+
assertThat(processed.get("strVal")).isInstanceOf(String.class);
290+
assertThat(processed.get("doubleVal")).isInstanceOf(Number.class).isEqualTo(3.14);
291+
assertThat(processed.get("listVal")).isInstanceOf(String.class).isEqualTo("[1,2,3]");
292+
assertThat(processed.get("mapVal")).isInstanceOf(String.class);
293+
assertThatJson(processed.get("mapVal")).isEqualTo("{a:1,b:2}");
294+
}
295+
278296
@SpringBootConfiguration
279297
public static class Config {
280298

0 commit comments

Comments
 (0)