Skip to content

Commit e954045

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents f17dfa1 + 5b195a4 commit e954045

File tree

12 files changed

+1029
-2
lines changed

12 files changed

+1029
-2
lines changed

advisors/spring-ai-advisors-vector-store/src/test/java/org/springframework/ai/chat/client/advisor/vectorstore/QuestionAnswerAdvisorTests.java

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,163 @@ public void qaAdvisorTakesUserParameterizedUserMessagesIntoAccountForSimilarityS
243243
Assertions.assertThat(this.vectorSearchCaptor.getValue().getQuery()).isEqualTo(expectedQuery);
244244
}
245245

246+
@Test
247+
public void qaAdvisorWithMultipleFilterParameters() {
248+
given(this.chatModel.call(this.promptCaptor.capture()))
249+
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("Filtered response"))),
250+
ChatResponseMetadata.builder().build()));
251+
252+
given(this.vectorStore.similaritySearch(this.vectorSearchCaptor.capture()))
253+
.willReturn(List.of(new Document("doc1"), new Document("doc2")));
254+
255+
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
256+
.searchRequest(SearchRequest.builder().topK(10).build())
257+
.build();
258+
259+
var chatClient = ChatClient.builder(this.chatModel)
260+
.defaultAdvisors(qaAdvisor)
261+
.build();
262+
263+
chatClient.prompt()
264+
.user("Complex query")
265+
.advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "type == 'Documentation' AND status == 'Published'"))
266+
.call()
267+
.chatResponse();
268+
269+
var capturedFilter = this.vectorSearchCaptor.getValue().getFilterExpression();
270+
assertThat(capturedFilter).isNotNull();
271+
// The filter should be properly constructed with AND operation
272+
assertThat(capturedFilter.toString()).contains("type");
273+
assertThat(capturedFilter.toString()).contains("Documentation");
274+
}
275+
276+
@Test
277+
public void qaAdvisorWithDifferentSimilarityThresholds() {
278+
given(this.chatModel.call(this.promptCaptor.capture()))
279+
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("High threshold response"))),
280+
ChatResponseMetadata.builder().build()));
281+
282+
given(this.vectorStore.similaritySearch(this.vectorSearchCaptor.capture()))
283+
.willReturn(List.of(new Document("relevant doc")));
284+
285+
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
286+
.searchRequest(SearchRequest.builder().similarityThreshold(0.95).topK(3).build())
287+
.build();
288+
289+
var chatClient = ChatClient.builder(this.chatModel)
290+
.defaultAdvisors(qaAdvisor)
291+
.build();
292+
293+
chatClient.prompt()
294+
.user("Specific question requiring high similarity")
295+
.call()
296+
.chatResponse();
297+
298+
assertThat(this.vectorSearchCaptor.getValue().getSimilarityThreshold()).isEqualTo(0.95);
299+
assertThat(this.vectorSearchCaptor.getValue().getTopK()).isEqualTo(3);
300+
}
301+
302+
@Test
303+
public void qaAdvisorWithComplexParameterizedTemplate() {
304+
given(this.chatModel.call(this.promptCaptor.capture()))
305+
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("Complex template response"))),
306+
ChatResponseMetadata.builder().build()));
307+
308+
given(this.vectorStore.similaritySearch(this.vectorSearchCaptor.capture()))
309+
.willReturn(List.of(new Document("template doc")));
310+
311+
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
312+
.searchRequest(SearchRequest.builder().build())
313+
.build();
314+
315+
var chatClient = ChatClient.builder(this.chatModel)
316+
.defaultAdvisors(qaAdvisor)
317+
.build();
318+
319+
var complexTemplate = "Please analyze {topic} considering {aspect1} and {aspect2} for user {userId}";
320+
chatClient.prompt()
321+
.user(u -> u.text(complexTemplate)
322+
.param("topic", "machine learning")
323+
.param("aspect1", "performance")
324+
.param("aspect2", "scalability")
325+
.param("userId", "user1"))
326+
.call()
327+
.chatResponse();
328+
329+
var expectedQuery = "Please analyze machine learning considering performance and scalability for user user1";
330+
assertThat(this.vectorSearchCaptor.getValue().getQuery()).isEqualTo(expectedQuery);
331+
332+
Message userMessage = this.promptCaptor.getValue().getInstructions().get(0);
333+
assertThat(userMessage.getText()).contains(expectedQuery);
334+
assertThat(userMessage.getText()).doesNotContain("{topic}");
335+
assertThat(userMessage.getText()).doesNotContain("{aspect1}");
336+
assertThat(userMessage.getText()).doesNotContain("{aspect2}");
337+
assertThat(userMessage.getText()).doesNotContain("{userId}");
338+
}
339+
340+
@Test
341+
public void qaAdvisorWithDocumentsContainingMetadata() {
342+
given(this.chatModel.call(this.promptCaptor.capture()))
343+
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("Metadata response"))),
344+
ChatResponseMetadata.builder().build()));
345+
346+
var docWithMetadata1 = new Document("First document content", Map.of("source", "wiki", "author", "John"));
347+
var docWithMetadata2 = new Document("Second document content", Map.of("source", "manual", "version", "2.1"));
348+
349+
given(this.vectorStore.similaritySearch(this.vectorSearchCaptor.capture()))
350+
.willReturn(List.of(docWithMetadata1, docWithMetadata2));
351+
352+
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
353+
.searchRequest(SearchRequest.builder().topK(2).build())
354+
.build();
355+
356+
var chatClient = ChatClient.builder(this.chatModel)
357+
.defaultAdvisors(qaAdvisor)
358+
.build();
359+
360+
chatClient.prompt()
361+
.user("Question about documents with metadata")
362+
.call()
363+
.chatResponse();
364+
365+
Message userMessage = this.promptCaptor.getValue().getInstructions().get(0);
366+
assertThat(userMessage.getText()).contains("First document content");
367+
assertThat(userMessage.getText()).contains("Second document content");
368+
}
369+
370+
@Test
371+
public void qaAdvisorBuilderValidation() {
372+
// Test that builder validates required parameters
373+
Assertions.assertThatThrownBy(() -> QuestionAnswerAdvisor.builder(null))
374+
.isInstanceOf(IllegalArgumentException.class);
375+
376+
// Test successful builder creation
377+
var advisor = QuestionAnswerAdvisor.builder(this.vectorStore).build();
378+
assertThat(advisor).isNotNull();
379+
}
380+
381+
@Test
382+
public void qaAdvisorWithZeroTopK() {
383+
given(this.chatModel.call(this.promptCaptor.capture()))
384+
.willReturn(new ChatResponse(List.of(new Generation(new AssistantMessage("Zero docs response"))),
385+
ChatResponseMetadata.builder().build()));
386+
387+
given(this.vectorStore.similaritySearch(this.vectorSearchCaptor.capture()))
388+
.willReturn(List.of());
389+
390+
var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
391+
.searchRequest(SearchRequest.builder().topK(0).build())
392+
.build();
393+
394+
var chatClient = ChatClient.builder(this.chatModel)
395+
.defaultAdvisors(qaAdvisor)
396+
.build();
397+
398+
chatClient.prompt()
399+
.user("Question with zero topK")
400+
.call()
401+
.chatResponse();
402+
403+
assertThat(this.vectorSearchCaptor.getValue().getTopK()).isEqualTo(0);
404+
}
246405
}

advisors/spring-ai-advisors-vector-store/src/test/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisorTests.java

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,155 @@ void whenBuilderWithCustomSystemPromptTemplateThenSuccess() {
206206
assertThat(advisor).isNotNull();
207207
}
208208

209+
@Test
210+
void whenBuilderWithEmptyStringConversationIdThenThrow() {
211+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
212+
213+
assertThatThrownBy(() -> VectorStoreChatMemoryAdvisor.builder(vectorStore).conversationId("").build())
214+
.isInstanceOf(IllegalArgumentException.class)
215+
.hasMessageContaining("defaultConversationId cannot be null or empty");
216+
}
217+
218+
@Test
219+
void whenBuilderWithWhitespaceOnlyConversationIdThenThrow() {
220+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
221+
222+
assertThatThrownBy(() -> VectorStoreChatMemoryAdvisor.builder(vectorStore).conversationId("\t\n\r ").build())
223+
.isInstanceOf(IllegalArgumentException.class)
224+
.hasMessageContaining("defaultConversationId cannot be null or empty");
225+
}
226+
227+
@Test
228+
void whenBuilderWithSpecialCharactersInConversationIdThenSuccess() {
229+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
230+
231+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
232+
.conversationId("[email protected]")
233+
.build();
234+
235+
assertThat(advisor).isNotNull();
236+
}
237+
238+
@Test
239+
void whenBuilderWithMaxIntegerTopKThenSuccess() {
240+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
241+
242+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
243+
.defaultTopK(Integer.MAX_VALUE)
244+
.build();
245+
246+
assertThat(advisor).isNotNull();
247+
}
248+
249+
@Test
250+
void whenBuilderWithNegativeTopKThenThrow() {
251+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
252+
253+
assertThatThrownBy(() -> VectorStoreChatMemoryAdvisor.builder(vectorStore).defaultTopK(-100).build())
254+
.isInstanceOf(IllegalArgumentException.class)
255+
.hasMessageContaining("topK must be greater than 0");
256+
}
257+
258+
@Test
259+
void whenBuilderChainedWithAllParametersThenSuccess() {
260+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
261+
Scheduler scheduler = Mockito.mock(Scheduler.class);
262+
PromptTemplate systemPromptTemplate = Mockito.mock(PromptTemplate.class);
263+
264+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
265+
.conversationId("chained-test")
266+
.defaultTopK(42)
267+
.scheduler(scheduler)
268+
.systemPromptTemplate(systemPromptTemplate)
269+
.build();
270+
271+
assertThat(advisor).isNotNull();
272+
}
273+
274+
@Test
275+
void whenBuilderParametersSetInDifferentOrderThenSuccess() {
276+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
277+
Scheduler scheduler = Mockito.mock(Scheduler.class);
278+
PromptTemplate systemPromptTemplate = Mockito.mock(PromptTemplate.class);
279+
280+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
281+
.systemPromptTemplate(systemPromptTemplate)
282+
.defaultTopK(7)
283+
.scheduler(scheduler)
284+
.conversationId("order-test")
285+
.build();
286+
287+
assertThat(advisor).isNotNull();
288+
}
289+
290+
@Test
291+
void whenBuilderWithOverriddenParametersThenUseLastValue() {
292+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
293+
294+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
295+
.conversationId("first-id")
296+
.conversationId("second-id") // This should override the first
297+
.defaultTopK(5)
298+
.defaultTopK(10) // This should override the first
299+
.build();
300+
301+
assertThat(advisor).isNotNull();
302+
}
303+
304+
@Test
305+
void whenBuilderReusedThenCreatesSeparateInstances() {
306+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
307+
308+
// Simulate builder reuse (if the builder itself is stateful)
309+
var builder = VectorStoreChatMemoryAdvisor.builder(vectorStore).conversationId("shared-config");
310+
311+
VectorStoreChatMemoryAdvisor advisor1 = builder.build();
312+
VectorStoreChatMemoryAdvisor advisor2 = builder.build();
313+
314+
assertThat(advisor1).isNotNull();
315+
assertThat(advisor2).isNotNull();
316+
assertThat(advisor1).isNotSameAs(advisor2);
317+
}
318+
319+
@Test
320+
void whenBuilderWithLongConversationIdThenSuccess() {
321+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
322+
String longId = "a".repeat(1000); // 1000 character conversation ID
323+
324+
VectorStoreChatMemoryAdvisor advisor = VectorStoreChatMemoryAdvisor.builder(vectorStore)
325+
.conversationId(longId)
326+
.build();
327+
328+
assertThat(advisor).isNotNull();
329+
}
330+
331+
@Test
332+
void whenBuilderCalledWithNullAfterValidValueThenThrow() {
333+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
334+
335+
assertThatThrownBy(() -> VectorStoreChatMemoryAdvisor.builder(vectorStore)
336+
.conversationId("valid-id")
337+
.conversationId(null) // Set to null after valid value
338+
.build()).isInstanceOf(IllegalArgumentException.class)
339+
.hasMessageContaining("defaultConversationId cannot be null or empty");
340+
}
341+
342+
@Test
343+
void whenBuilderWithTopKBoundaryValuesThenSuccess() {
344+
VectorStore vectorStore = Mockito.mock(VectorStore.class);
345+
346+
// Test with value 1 (minimum valid)
347+
VectorStoreChatMemoryAdvisor advisor1 = VectorStoreChatMemoryAdvisor.builder(vectorStore)
348+
.defaultTopK(1)
349+
.build();
350+
351+
// Test with a reasonable upper bound
352+
VectorStoreChatMemoryAdvisor advisor2 = VectorStoreChatMemoryAdvisor.builder(vectorStore)
353+
.defaultTopK(10000)
354+
.build();
355+
356+
assertThat(advisor1).isNotNull();
357+
assertThat(advisor2).isNotNull();
358+
}
359+
209360
}

models/spring-ai-google-genai/src/test/java/org/springframework/ai/google/genai/MimeTypeDetectorTests.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
import org.junit.jupiter.params.provider.Arguments;
2727
import org.junit.jupiter.params.provider.MethodSource;
2828

29-
import org.springframework.ai.google.genai.MimeTypeDetector;
29+
import org.junit.jupiter.params.provider.ValueSource;
3030
import org.springframework.core.io.PathResource;
3131
import org.springframework.util.MimeType;
3232

3333
import static org.assertj.core.api.Assertions.assertThat;
34+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
35+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
3436
import static org.springframework.ai.google.genai.MimeTypeDetector.GEMINI_MIME_TYPES;
3537

3638
/**
@@ -90,4 +92,49 @@ void getMimeTypeByString(String extension, MimeType expectedMimeType) {
9092
assertThat(mimeType).isEqualTo(expectedMimeType);
9193
}
9294

95+
@ParameterizedTest
96+
@ValueSource(strings = { " ", "\t", "\n" })
97+
void getMimeTypeByStringWithInvalidInputShouldThrowException(String invalidPath) {
98+
assertThatThrownBy(() -> MimeTypeDetector.getMimeType(invalidPath)).isInstanceOf(IllegalArgumentException.class)
99+
.hasMessageContaining("Unable to detect the MIME type");
100+
}
101+
102+
@ParameterizedTest
103+
@ValueSource(strings = { "JPG", "PNG", "GIF" })
104+
void getMimeTypeByStringWithUppercaseExtensionsShouldWork(String uppercaseExt) {
105+
String upperFileName = "test." + uppercaseExt;
106+
String lowerFileName = "test." + uppercaseExt.toLowerCase();
107+
108+
// Should throw for uppercase (not in map) but work for lowercase
109+
assertThatThrownBy(() -> MimeTypeDetector.getMimeType(upperFileName))
110+
.isInstanceOf(IllegalArgumentException.class);
111+
112+
// Lowercase should work if it's a supported extension
113+
if (GEMINI_MIME_TYPES.containsKey(uppercaseExt.toLowerCase())) {
114+
assertThatCode(() -> MimeTypeDetector.getMimeType(lowerFileName)).doesNotThrowAnyException();
115+
}
116+
}
117+
118+
@ParameterizedTest
119+
@ValueSource(strings = { "test.jpg", "test.png", "test.gif" })
120+
void getMimeTypeSupportedFileAcrossDifferentMethodsShouldBeConsistent(String fileName) {
121+
MimeType stringResult = MimeTypeDetector.getMimeType(fileName);
122+
MimeType fileResult = MimeTypeDetector.getMimeType(new File(fileName));
123+
MimeType pathResult = MimeTypeDetector.getMimeType(Path.of(fileName));
124+
125+
// All methods should return the same result for supported extensions
126+
assertThat(stringResult).isEqualTo(fileResult);
127+
assertThat(stringResult).isEqualTo(pathResult);
128+
}
129+
130+
@ParameterizedTest
131+
@ValueSource(strings = { "https://example.com/documents/file.pdf", "https://example.com/data/file.json",
132+
"https://example.com/files/document.txt" })
133+
void getMimeTypeByURIWithUnsupportedExtensionsShouldThrowException(String url) {
134+
URI uri = URI.create(url);
135+
136+
assertThatThrownBy(() -> MimeTypeDetector.getMimeType(uri)).isInstanceOf(IllegalArgumentException.class)
137+
.hasMessageContaining("Unable to detect the MIME type");
138+
}
139+
93140
}

0 commit comments

Comments
 (0)