From ae09200114d704e24f9409824613f5969bbc1297 Mon Sep 17 00:00:00 2001 From: Sun Yuhan Date: Wed, 27 Aug 2025 20:56:51 +0800 Subject: [PATCH 1/4] feat: GH-4251 Add a `getRequiredVariables` method to `TemplateRenderer` to parse the required variables from the template. Signed-off-by: Sun Yuhan --- .../openai/chat/OpenAiCompatibleChatModelIT.java | 2 +- .../ai/template/NoOpTemplateRenderer.java | 6 ++++++ .../ai/template/TemplateRenderer.java | 4 ++++ .../ai/chat/prompt/PromptTemplateTests.java | 6 ++++++ .../ai/chat/prompt/SystemPromptTemplateTests.java | 6 ++++++ .../ai/template/st/StTemplateRenderer.java | 6 ++++++ .../ai/template/st/StTemplateRendererTests.java | 14 ++++++++++++++ 7 files changed, 43 insertions(+), 1 deletion(-) diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiCompatibleChatModelIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiCompatibleChatModelIT.java index b83db6387b6..7b11baf0db4 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiCompatibleChatModelIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiCompatibleChatModelIT.java @@ -68,7 +68,7 @@ static Stream openAiCompatibleApis() { .openAiApi(OpenAiApi.builder() .baseUrl("https://api.groq.com/openai") .apiKey(System.getenv("GROQ_API_KEY")) - .build()) + .build()) .defaultOptions(forModelName("llama3-8b-8192")) .build()); } diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/template/NoOpTemplateRenderer.java b/spring-ai-commons/src/main/java/org/springframework/ai/template/NoOpTemplateRenderer.java index d4685201cf7..2af72b087ec 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/template/NoOpTemplateRenderer.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/template/NoOpTemplateRenderer.java @@ -17,6 +17,7 @@ package org.springframework.ai.template; import java.util.Map; +import java.util.Set; import org.springframework.util.Assert; @@ -36,4 +37,9 @@ public String apply(String template, Map variables) { return template; } + @Override + public Set getRequiredVariables(String template) { + return Set.of(); + } + } diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java b/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java index 96b199af43a..2d5a6302f73 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java @@ -17,12 +17,14 @@ package org.springframework.ai.template; import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; /** * Renders a template using a given strategy. * * @author Thomas Vitale + * @author Sun Yuhan * @since 1.0.0 */ public interface TemplateRenderer extends BiFunction, String> { @@ -30,4 +32,6 @@ public interface TemplateRenderer extends BiFunction @Override String apply(String template, Map variables); + Set getRequiredVariables(String template); + } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/PromptTemplateTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/PromptTemplateTests.java index ecd33e0317d..749b44e1122 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/PromptTemplateTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/PromptTemplateTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -316,6 +317,11 @@ public String apply(String template, Map model) { return template + " (Rendered by Custom)"; } + @Override + public Set getRequiredVariables(String template) { + return Set.of(); + } + } } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java index d5b08064d12..0ff41511933 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java @@ -26,6 +26,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -324,6 +325,11 @@ public String apply(String template, Map model) { return template + " (Rendered by Custom)"; } + @Override + public Set getRequiredVariables(String template) { + return Set.of(); + } + } } diff --git a/spring-ai-template-st/src/main/java/org/springframework/ai/template/st/StTemplateRenderer.java b/spring-ai-template-st/src/main/java/org/springframework/ai/template/st/StTemplateRenderer.java index 3780b948a09..33f102e03c5 100644 --- a/spring-ai-template-st/src/main/java/org/springframework/ai/template/st/StTemplateRenderer.java +++ b/spring-ai-template-st/src/main/java/org/springframework/ai/template/st/StTemplateRenderer.java @@ -49,6 +49,7 @@ * is shared between threads. * * @author Thomas Vitale + * @author Sun Yuhan * @since 1.0.0 */ public class StTemplateRenderer implements TemplateRenderer { @@ -110,6 +111,11 @@ public String apply(String template, Map variables) { return st.render(); } + @Override + public Set getRequiredVariables(String template) { + return getInputVariables(createST(template)); + } + private ST createST(String template) { try { return new ST(template, this.startDelimiterToken, this.endDelimiterToken); diff --git a/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java b/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java index 1dd548c5c0e..1a8f646d3dd 100644 --- a/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java +++ b/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -31,6 +32,7 @@ * Unit tests for {@link StTemplateRenderer}. * * @author Thomas Vitale + * @author Sun Yuhan */ class StTemplateRendererTests { @@ -297,4 +299,16 @@ void shouldRenderTemplateWithBuiltInFunctions() { assertThat(result).isEqualTo("Hello!"); } + /** + * Test whether the required variables can be correctly extracted from the template. + */ + @Test + void shouldCorrectlyExtractedRequiredVariables() { + StTemplateRenderer renderer = StTemplateRenderer.builder().build(); + String template = "Person: {name}, Age: {age}"; + Set requiredVariables = renderer.getRequiredVariables(template); + + assertThat(requiredVariables).contains("name", "age"); + } + } From 04312df2fb220f232916ce21dad958842b8c02ed Mon Sep 17 00:00:00 2001 From: Sun Yuhan Date: Thu, 28 Aug 2025 09:39:58 +0800 Subject: [PATCH 2/4] fix: Remove the redundant imports Signed-off-by: Sun Yuhan --- .../ai/chat/prompt/SystemPromptTemplateTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java index 9acb85a88d8..505ee833075 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java @@ -28,8 +28,6 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; -import java.util.HashMap; -import java.util.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; From 9d60992f6b2f2d9943126c3a99bc69fa1829509c Mon Sep 17 00:00:00 2001 From: Sun Yuhan Date: Thu, 28 Aug 2025 10:38:02 +0800 Subject: [PATCH 3/4] fix: Fix the import order. Signed-off-by: Sun Yuhan --- .../ai/chat/prompt/SystemPromptTemplateTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java index 505ee833075..359b4e6c684 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/prompt/SystemPromptTemplateTests.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -28,8 +29,6 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; -import java.util.Set; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; From 2e6e126ec071db6d30a497013ab132d7e20efc8a Mon Sep 17 00:00:00 2001 From: Sun Yuhan Date: Thu, 2 Oct 2025 19:43:54 +0800 Subject: [PATCH 4/4] fix: Merged the main branch code and adjusted `getRequiredVariables` to handle cases where subclasses do not support this method. Signed-off-by: Sun Yuhan --- .../org/springframework/ai/template/TemplateRenderer.java | 6 +++++- .../ai/template/st/StTemplateRendererTests.java | 8 +++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java b/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java index 2d5a6302f73..353d41a9a13 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/template/TemplateRenderer.java @@ -20,6 +20,8 @@ import java.util.Set; import java.util.function.BiFunction; +import javax.naming.OperationNotSupportedException; + /** * Renders a template using a given strategy. * @@ -32,6 +34,8 @@ public interface TemplateRenderer extends BiFunction @Override String apply(String template, Map variables); - Set getRequiredVariables(String template); + default Set getRequiredVariables(String template) throws OperationNotSupportedException { + throw new OperationNotSupportedException("getRequiredVariables is not supported"); + } } diff --git a/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java b/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java index 8ed678a26d6..c5021e54b52 100644 --- a/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java +++ b/spring-ai-template-st/src/test/java/org/springframework/ai/template/st/StTemplateRendererTests.java @@ -299,7 +299,6 @@ void shouldRenderTemplateWithBuiltInFunctions() { assertThat(result).isEqualTo("Hello!"); } - /** * Tests that property access syntax like {test.name} is correctly handled. The * top-level variable 'test' should be identified as required, but 'name' should not. @@ -351,12 +350,11 @@ void shouldValidatePropertyAccessCorrectly() { missingVariables.put("profile", Map.of("name", "John")); assertThatThrownBy(() -> renderer.apply("Hello {user.profile.name}!", missingVariables)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining( - "Not all variables were replaced in the template. Missing variable names are: [user]"); + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining( + "Not all variables were replaced in the template. Missing variable names are: [user]"); } - /** * Test whether the required variables can be correctly extracted from the template. */