Skip to content

Commit 0a10dca

Browse files
committed
Added annotations for retry for qos provider, and also properties, with nested for action names gh-1221
Signed-off-by: hayden.rear <hayden.rear@gmail.com>
1 parent 4c156f5 commit 0a10dca

File tree

15 files changed

+928
-5
lines changed

15 files changed

+928
-5
lines changed

embabel-agent-api/src/main/kotlin/com/embabel/agent/api/annotation/annotations.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.embabel.agent.api.annotation
1717

1818
import com.embabel.agent.api.common.PlannerType
1919
import com.embabel.agent.core.IoBinding
20+
import com.embabel.agent.core.ActionRetryPolicy
2021
import com.embabel.common.core.types.Semver.Companion.DEFAULT_VERSION
2122
import com.embabel.common.core.types.ZeroToOne
2223
import org.springframework.core.annotation.AliasFor
@@ -57,6 +58,17 @@ annotation class EmbabelComponent(
5758
* to be turned into a Spring bean in case of an autodetected component. Use only if there's the likelihood of
5859
* conflict with the default bean name.
5960
* @param opaque Whether to hide the agent's actions and conditions
61+
* @param actionRetryPolicy {@link com.embabel.agent.core.ActionRetryPolicy} for how to manage retries per action.
62+
* Use actionRetryPolicyExpression to specify specific properties. You can override this per action in the {@link Action} annotation.
63+
* @param actionRetryPolicyExpression An expression pointing to a set of properties for how to manage retries per action
64+
* overriding these (these are the defaults if you do not specify):
65+
* max-attempts: int = 5
66+
* backoff-millis: long = 10000
67+
* backoff-multiplier: double = 5.0
68+
* backoff-maxInterval: long = 60000
69+
* idempotent: boolean = false
70+
* example: ${agent.action-retry.default}
71+
* You can override this per action in the {@link Action} annotation.
6072
*/
6173
@Retention(AnnotationRetention.RUNTIME)
6274
@Target(
@@ -73,6 +85,8 @@ annotation class Agent(
7385
@get:AliasFor(annotation = Component::class, attribute = "value")
7486
val beanName: String = "",
7587
val opaque: Boolean = false,
88+
val actionRetryPolicy: ActionRetryPolicy = ActionRetryPolicy.DEFAULT,
89+
val actionRetryPolicyExpression: String = "",
7690
)
7791

7892
/**
@@ -163,6 +177,18 @@ annotation class ToolGroup(
163177
* is freshly added, even when multiple parameters of various types are available.
164178
* Defaults to Unit::class (no trigger). A trigger is an **additional** precondition: it
165179
* must be satisfied in addition to any preconditions listed in [pre] and the action method's input parameters.
180+
* @param actionRetryPolicy {@link com.embabel.agent.core.ActionRetryPolicy} for how to manage retries for this action.
181+
* Use actionRetryPolicyExpression
182+
* to specify specific properties.
183+
* @param actionRetryPolicyExpression An expression pointing to a set of properties for how to manage retries for this
184+
* action overriding these (these are the defaults if you do not specify):
185+
* max-attempts: int = 5
186+
* backoff-millis: long = 10000
187+
* backoff-multiplier: double = 5.0
188+
* backoff-maxInterval: long = 60000
189+
* idempotent: boolean = false
190+
* example: ${agent.action-retry.default}
191+
* These take precedence over specifying the default in the Agent annotation.
166192
*/
167193
@Target(AnnotationTarget.FUNCTION)
168194
@Retention(AnnotationRetention.RUNTIME)
@@ -181,6 +207,8 @@ annotation class Action(
181207
val toolGroups: Array<String> = [],
182208
val toolGroupRequirements: Array<ToolGroup> = [],
183209
val trigger: KClass<*> = Unit::class,
210+
val actionRetryPolicy: ActionRetryPolicy = ActionRetryPolicy.DEFAULT,
211+
val actionRetryPolicyExpression: String = "",
184212
)
185213

186214
/**

embabel-agent-api/src/main/kotlin/com/embabel/agent/api/annotation/support/ActionMethodManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ interface ActionMethodManager {
4444
*/
4545
val argumentResolvers: List<ActionMethodArgumentResolver>
4646

47+
val actionQosProvider: ActionQosProvider
48+
4749
/**
4850
* Create an Action from a method
4951
* @param method the method to create an action from
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.api.annotation.support
17+
18+
import com.embabel.agent.spi.config.spring.AgentPlatformProperties
19+
import org.slf4j.LoggerFactory
20+
import org.springframework.boot.context.properties.bind.Binder
21+
import org.springframework.core.env.Environment
22+
import org.springframework.stereotype.Component
23+
24+
@Component
25+
class ActionQosPropertyProvider(val env: Environment? = null) {
26+
27+
private val logger = LoggerFactory.getLogger(ActionQosPropertyProvider::class.java)
28+
29+
private val propertiesBinder: Binder? by lazy(LazyThreadSafetyMode.PUBLICATION) {
30+
env?.let (Binder::get)
31+
}
32+
33+
fun getBound(expr: String): AgentPlatformProperties.ActionQosProperties.ActionProperties? {
34+
if (expr.isBlank()) return null
35+
val propertiesBinder = propertiesBinder ?: return null
36+
37+
val prefix = if (expr.startsWith("\${") && expr.endsWith("}")) {
38+
expr.substring(2, expr.length - 1).trim()
39+
} else {
40+
expr
41+
}
42+
43+
val prefixResolved = env.let {
44+
if (it == null)
45+
expr
46+
else
47+
resolvePrefix(it, prefix) ?: prefix
48+
}
49+
50+
return propertiesBinder.bind(prefixResolved, AgentPlatformProperties.ActionQosProperties.ActionProperties::class.java)
51+
?.orElse(null)
52+
}
53+
54+
private fun resolvePrefix(env: Environment, s: String): String? {
55+
if (env.containsProperty(s)) {
56+
return s
57+
}
58+
59+
logger.info("Did not find prefix $s when resolving action properties.")
60+
return null
61+
}
62+
63+
}

embabel-agent-api/src/main/kotlin/com/embabel/agent/api/annotation/support/ActionQosProvider.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ package com.embabel.agent.api.annotation.support
1818
import com.embabel.agent.core.ActionQos
1919
import java.lang.reflect.Method
2020

21+
/**
22+
* Provides {@link com.embabel.agent.core.ActionQos} for a method, typically derived from
23+
* {@link com.embabel.agent.api.annotation.Action} and {@link com.embabel.agent.api.annotation.Agent} metadata.
24+
*/
2125
interface ActionQosProvider {
2226

23-
fun provideActionQos(method: Method,
24-
instance: Any): ActionQos {
27+
fun provideActionQos(
28+
method: Method,
29+
instance: Any
30+
): ActionQos {
2531
return ActionQos()
2632
}
2733

embabel-agent-api/src/main/kotlin/com/embabel/agent/api/annotation/support/DefaultActionMethodManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import kotlin.reflect.jvm.kotlinFunction
4848
@Component
4949
internal class DefaultActionMethodManager(
5050
val nameGenerator: MethodDefinedOperationNameGenerator = MethodDefinedOperationNameGenerator(),
51-
val actionQosProvider: ActionQosProvider = DefaultActionQosProvider(),
51+
override val actionQosProvider: ActionQosProvider = DefaultActionQosProvider(),
5252
override val argumentResolvers: List<ActionMethodArgumentResolver> = listOf(
5353
ProcessContextArgumentResolver(),
5454
OperationContextArgumentResolver(),

embabel-agent-api/src/main/kotlin/com/embabel/agent/api/annotation/support/DefaultActionQosProvider.kt

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,63 @@
1515
*/
1616
package com.embabel.agent.api.annotation.support
1717

18+
import com.embabel.agent.api.annotation.Action
19+
import com.embabel.agent.api.annotation.Agent
1820
import com.embabel.agent.core.ActionQos
21+
import com.embabel.agent.core.ActionRetryPolicy
22+
import com.embabel.agent.spi.config.spring.AgentPlatformProperties
23+
import org.springframework.stereotype.Component
1924
import java.lang.reflect.Method
2025

21-
class DefaultActionQosProvider : ActionQosProvider {
26+
/**
27+
* Default {@link com.embabel.agent.api.annotation.support.ActionQosProvider} implementation that resolves
28+
* retry overrides from {@link com.embabel.agent.api.annotation.Agent} and {@link com.embabel.agent.api.annotation.Action},
29+
* then maps them to {@link com.embabel.agent.core.ActionQos}.
30+
*/
31+
@Component
32+
class DefaultActionQosProvider(
33+
val actionQosProperties: AgentPlatformProperties.ActionQosProperties = AgentPlatformProperties.ActionQosProperties(),
34+
val propertyProvider: ActionQosPropertyProvider = ActionQosPropertyProvider(),
35+
) : ActionQosProvider {
36+
2237
override fun provideActionQos(
2338
method: Method,
2439
instance: Any
2540
): ActionQos {
26-
return ActionQos()
41+
42+
var defaultActionQos = actionQosProperties.default
43+
44+
var props = instance.javaClass.getAnnotation(Agent::class.java)?.let {
45+
if (hasRetryExpression(it.actionRetryPolicyExpression)) {
46+
defaultActionQos = defaultActionQos
47+
.overridingNotNull(getBound(it.actionRetryPolicyExpression))
48+
}
49+
50+
if (it.actionRetryPolicy == ActionRetryPolicy.FIRE_ONCE) {
51+
defaultActionQos = defaultActionQos.copy(maxAttempts = 1)
52+
}
53+
54+
defaultActionQos
55+
56+
} ?: defaultActionQos
57+
58+
method.getAnnotation(Action::class.java)?.let {
59+
if (hasRetryExpression(it.actionRetryPolicyExpression)) {
60+
props = props.overridingNotNull(getBound(it.actionRetryPolicyExpression))
61+
}
62+
if (it.actionRetryPolicy == ActionRetryPolicy.FIRE_ONCE) {
63+
props = props.copy(maxAttempts = 1)
64+
}
65+
}
66+
67+
68+
return props.toActionQos()
2769
}
70+
71+
72+
fun getBound(expr: String): AgentPlatformProperties.ActionQosProperties.ActionProperties? {
73+
return propertyProvider.getBound(expr)
74+
}
75+
76+
private fun hasRetryExpression(expr: String): Boolean = expr.isNotBlank()
2877
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.core
17+
18+
/**
19+
* Retry policy selector for an action.
20+
*
21+
* This is the first-priority override for retry behavior on an {@link com.embabel.agent.api.annotation.Action}
22+
* or {@link com.embabel.agent.api.annotation.Agent}. The underlying policy maps to {@link ActionQos} with the
23+
* following default properties:
24+
* max-attempts: int = 5
25+
* backoff-millis: long = 10000
26+
* backoff-multiplier: double = 5.0
27+
* backoff-maxInterval: long = 60000
28+
* idempotent: boolean = false
29+
* To override with a custom policy, see actionRetryPolicyExpression on {@link com.embabel.agent.api.annotation.Action}
30+
* or {@link com.embabel.agent.api.annotation.Agent}.
31+
*/
32+
enum class ActionRetryPolicy {
33+
34+
/**
35+
* Fire only once: maps to {@link ActionQos} with maxAttempts = 1.
36+
*/
37+
FIRE_ONCE,
38+
39+
/**
40+
* Default retry policy: uses the default {@link ActionQos}. Note that using this retry policy explicitly will not
41+
* override any custom retry policy provided at any level, even if that custom retry policy is at a lower
42+
* precedence than the one annotated with this retry policy.
43+
*/
44+
DEFAULT
45+
46+
}

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

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

18+
import com.embabel.agent.core.ActionQos
1819
import org.springframework.boot.context.properties.ConfigurationProperties
1920
import org.springframework.boot.context.properties.NestedConfigurationProperty
2021

@@ -67,6 +68,9 @@ class AgentPlatformProperties {
6768
@field:NestedConfigurationProperty
6869
var test: TestConfig = TestConfig()
6970

71+
@field:NestedConfigurationProperty
72+
var actionQos: ActionQosProperties = ActionQosProperties()
73+
7074
/**
7175
* Agent Process Type
7276
*/
@@ -282,4 +286,58 @@ class AgentPlatformProperties {
282286
*/
283287
var mockMode: Boolean = true
284288
}
289+
290+
/**
291+
* Configuration of retry policy overrides for actions on agents.
292+
*
293+
* This allows configuring default and per-action overrides that map to {@link com.embabel.agent.core.ActionQos}.
294+
*/
295+
@ConfigurationProperties(prefix = "embabel.agent.platform.action-qos")
296+
class ActionQosProperties {
297+
298+
/**
299+
* Overrides for a single action's QoS settings.
300+
*
301+
* Null values mean "use defaults" (either the configured defaults or {@link com.embabel.agent.core.ActionQos}).
302+
*/
303+
data class ActionProperties(
304+
var maxAttempts: Int? = null,
305+
var backoffMillis: Long? = null,
306+
var backoffMultiplier: Double? = null,
307+
var backoffMaxInterval: Long? = null,
308+
var idempotent: Boolean? = null,
309+
) {
310+
fun overridingNotNull(overridingAction: ActionProperties?): ActionProperties {
311+
if (overridingAction == null) {
312+
return this
313+
}
314+
315+
return ActionProperties(
316+
maxAttempts = overridingAction.maxAttempts ?: this.maxAttempts,
317+
backoffMillis = overridingAction.backoffMillis ?: this.backoffMillis,
318+
backoffMultiplier = overridingAction.backoffMultiplier ?: backoffMultiplier,
319+
backoffMaxInterval = overridingAction.backoffMaxInterval ?: backoffMaxInterval,
320+
idempotent = overridingAction.idempotent ?: idempotent
321+
)
322+
}
323+
324+
fun toActionQos(defaultAction: ActionQos = ActionQos()): ActionQos {
325+
return ActionQos(
326+
maxAttempts = maxAttempts ?: defaultAction.maxAttempts,
327+
backoffMillis = backoffMillis ?: defaultAction.backoffMillis,
328+
backoffMultiplier = backoffMultiplier ?: defaultAction.backoffMultiplier,
329+
backoffMaxInterval = backoffMaxInterval ?: defaultAction.backoffMaxInterval,
330+
idempotent = idempotent ?: defaultAction.idempotent
331+
)
332+
}
333+
}
334+
335+
/**
336+
* Fallback retry properties for {@code @Action} and {@code @Agent} overrides.
337+
*
338+
* These values are merged with {@link com.embabel.agent.core.ActionQos} defaults.
339+
*/
340+
var default: ActionProperties = ActionProperties()
341+
342+
}
285343
}

0 commit comments

Comments
 (0)