Skip to content

Commit 6988f28

Browse files
authored
Configure nested template interpretation (#1978)
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
1 parent 10d5dde commit 6988f28

6 files changed

Lines changed: 142 additions & 2 deletions

File tree

embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/config/spring/AgentPlatformConfiguration.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ class AgentPlatformConfiguration(
101101
}
102102

103103
@Bean
104-
fun templateRenderer(): TemplateRenderer = JinjavaTemplateRenderer()
104+
@ConditionalOnMissingBean(TemplateRenderer::class)
105+
fun templateRenderer(properties: AgentPlatformProperties): TemplateRenderer =
106+
JinjavaTemplateRenderer(properties.template)
105107

106108
/**
107109
* Fallback if we don't have a more interesting logger

embabel-agent-api/src/main/kotlin/com/embabel/agent/spi/config/spring/AgentPlatformProperties.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package com.embabel.agent.spi.config.spring
1717

1818
import com.embabel.agent.core.ActionQos
19+
import com.embabel.common.textio.template.JinjaProperties
1920
import org.springframework.boot.context.properties.ConfigurationProperties
2021
import org.springframework.boot.context.properties.NestedConfigurationProperty
2122

@@ -53,6 +54,9 @@ class AgentPlatformProperties {
5354
@field:NestedConfigurationProperty
5455
var llmOperations: LlmOperationsConfig = LlmOperationsConfig()
5556

57+
@field:NestedConfigurationProperty
58+
var template: JinjaProperties = JinjaProperties()
59+
5660
@field:NestedConfigurationProperty
5761
var processIdGeneration: ProcessIdGenerationConfig = ProcessIdGenerationConfig()
5862

embabel-agent-api/src/test/kotlin/com/embabel/agent/config/AgentPlatformPropertiesIntegrationTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.springframework.boot.context.properties.bind.Binder
2727
import org.springframework.boot.test.context.SpringBootTest
2828
import org.springframework.context.annotation.Bean
2929
import org.springframework.core.env.Environment
30+
import org.springframework.mock.env.MockEnvironment
3031
import org.springframework.test.context.ActiveProfiles
3132
import org.springframework.test.context.TestPropertySource
3233

@@ -173,6 +174,10 @@ import org.springframework.test.context.TestPropertySource
173174
"embabel.agent.platform.llm-operations.prompts.generate-examples-by-default=false",
174175
"embabel.agent.platform.llm-operations.data-binding.max-attempts=20",
175176
"embabel.agent.platform.llm-operations.data-binding.fixed-backoff-millis=50",
177+
"embabel.agent.platform.template.prefix=classpath:/custom-prompts/",
178+
"embabel.agent.platform.template.suffix=.j2",
179+
"embabel.agent.platform.template.fail-on-unknown-tokens=true",
180+
"embabel.agent.platform.template.nested-interpretation-enabled=true",
176181
"embabel.agent.platform.models.anthropic.max-attempts=8",
177182
"embabel.agent.platform.models.anthropic.backoff-millis=3000",
178183
"embabel.agent.platform.models.openai.max-attempts=12",
@@ -257,6 +262,29 @@ class AgentPlatformPropertiesIntegrationTest {
257262
assertThat(properties.llmOperations.dataBinding.fixedBackoffMillis).isEqualTo(50L)
258263
}
259264

265+
@Test
266+
fun `should bind template properties correctly`() {
267+
assertThat(properties.template.prefix).isEqualTo("classpath:/custom-prompts/")
268+
assertThat(properties.template.suffix).isEqualTo(".j2")
269+
assertThat(properties.template.failOnUnknownTokens).isTrue()
270+
assertThat(properties.template.nestedInterpretationEnabled).isTrue()
271+
}
272+
273+
@Test
274+
fun `should bind a partial template configuration using defaults`() {
275+
val environment = MockEnvironment()
276+
.withProperty("embabel.agent.platform.template.nested-interpretation-enabled", "true")
277+
278+
val bound = Binder.get(environment)
279+
.bind("embabel.agent.platform", AgentPlatformProperties::class.java)
280+
.get()
281+
282+
assertThat(bound.template.prefix).isEqualTo("classpath:/prompts/")
283+
assertThat(bound.template.suffix).isEqualTo(".jinja")
284+
assertThat(bound.template.failOnUnknownTokens).isFalse()
285+
assertThat(bound.template.nestedInterpretationEnabled).isTrue()
286+
}
287+
260288
@Test
261289
fun `should bind model provider properties correctly`() {
262290
assertThat(properties.models.anthropic.maxAttempts).isEqualTo(8)
@@ -284,6 +312,10 @@ class AgentPlatformPropertiesIntegrationTest {
284312
assertThat(defaultProperties.scanning.annotation).isTrue()
285313
assertThat(defaultProperties.ranking.maxAttempts).isEqualTo(5)
286314
assertThat(defaultProperties.autonomy.agentConfidenceCutOff).isEqualTo(0.6)
315+
assertThat(defaultProperties.template.prefix).isEqualTo("classpath:/prompts/")
316+
assertThat(defaultProperties.template.suffix).isEqualTo(".jinja")
317+
assertThat(defaultProperties.template.failOnUnknownTokens).isFalse()
318+
assertThat(defaultProperties.template.nestedInterpretationEnabled).isFalse()
287319
assertThat(defaultProperties.models.anthropic.maxAttempts).isEqualTo(10)
288320
assertThat(defaultProperties.models.openai.maxAttempts).isEqualTo(10)
289321
assertThat(defaultProperties.test.mockMode).isTrue()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2024-2026 Embabel Pty Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.embabel.agent.spi.config.spring
17+
18+
import com.embabel.common.textio.template.JinjaProperties
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.jupiter.api.Test
21+
22+
class AgentPlatformConfigurationTemplateTest {
23+
24+
private val configuration = AgentPlatformConfiguration()
25+
26+
@Test
27+
fun `template renderer should preserve nested template syntax by default`() {
28+
val renderer = configuration.templateRenderer(AgentPlatformProperties())
29+
30+
val result = renderer.renderLiteralTemplate(
31+
"{{ message }}",
32+
mapOf("message" to "compute {{ 7*7 }} now"),
33+
)
34+
35+
assertThat(result).isEqualTo("compute {{ 7*7 }} now")
36+
}
37+
38+
@Test
39+
fun `template renderer should support explicitly enabled nested interpretation`() {
40+
val properties = AgentPlatformProperties().apply {
41+
template = JinjaProperties(nestedInterpretationEnabled = true)
42+
}
43+
val renderer = configuration.templateRenderer(properties)
44+
45+
val result = renderer.renderLiteralTemplate(
46+
"{{ message }}",
47+
mapOf("message" to "compute {{ 7*7 }} now"),
48+
)
49+
50+
assertThat(result).isEqualTo("compute 49 now")
51+
}
52+
}

embabel-agent-docs/src/main/asciidoc/reference/configuration/page.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,36 @@ TIP: For models with extended thinking enabled (like Claude with thinking mode),
731731

732732
NOTE: The HTTP client configuration applies to all model providers that use Spring's `RestClient` and `WebClient`, including OpenAI, Anthropic, and OpenAI-compatible endpoints.
733733

734+
===== Template Renderer
735+
736+
From the `JinjaProperties` nested in `AgentPlatformProperties` - Jinja template renderer configuration.
737+
738+
[cols="3,2,1,4",options="header"]
739+
|===
740+
|Property |Type |Default |Description
741+
742+
|`embabel.agent.platform.template.prefix`
743+
|String
744+
|`classpath:/prompts/`
745+
|Spring resource prefix used to load templates
746+
747+
|`embabel.agent.platform.template.suffix`
748+
|String
749+
|`.jinja`
750+
|Suffix added to template names that do not already include it
751+
752+
|`embabel.agent.platform.template.fail-on-unknown-tokens`
753+
|Boolean
754+
|`false`
755+
|Whether rendering fails when a template contains unknown tokens
756+
757+
|`embabel.agent.platform.template.nested-interpretation-enabled`
758+
|Boolean
759+
|`false`
760+
|Whether template syntax inside substituted values is evaluated recursively. Do not enable this for untrusted or user-provided values.
761+
762+
|===
763+
734764
===== Server-Sent Events
735765

736766
From `AgentPlatformProperties.SseConfig` - server-sent events configuration.

embabel-agent-docs/src/main/asciidoc/reference/templates/page.adoc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,26 @@ You can also specify a full resource path with https://docs.spring.io/spring-fra
1212

1313
Once you have specified the template, you can create objects using a model map.
1414

15+
Template syntax inside substituted values is treated as literal data by default.
16+
This prevents values such as user-provided text containing `{{ expression }}` from being evaluated recursively.
17+
18+
The renderer can be configured under `embabel.agent.platform.template`:
19+
20+
[source,yaml]
21+
----
22+
embabel:
23+
agent:
24+
platform:
25+
template:
26+
prefix: "classpath:/prompts/"
27+
suffix: ".jinja"
28+
fail-on-unknown-tokens: false
29+
nested-interpretation-enabled: false
30+
----
31+
32+
WARNING: Enabling `nested-interpretation-enabled` re-evaluates template syntax found inside substituted values.
33+
Do not enable it for templates that interpolate untrusted or user-provided text.
34+
1535
An example:
1636

1737
[tabs]
@@ -125,4 +145,4 @@ val result = context.ai()
125145

126146
TIP: Don't rush to externalize prompts.
127147
In modern languages with multi-line strings, it's often easier to keep prompts in the codebase.
128-
Externalizing them can sacrifice type safety and lead to complexity and maintenance challenges.
148+
Externalizing them can sacrifice type safety and lead to complexity and maintenance challenges.

0 commit comments

Comments
 (0)