Skip to content

Commit 6808b6c

Browse files
authored
Merge pull request #225 from ehsavoie/missing_parameters
feature: Add optional and summarizedContext attributes to all agent a…
2 parents 64c3518 + 2f0397b commit 6808b6c

17 files changed

Lines changed: 358 additions & 10 deletions

langchain4j-cdi-a2a/src/main/java/dev/langchain4j/cdi/agent/a2a/DefaultA2AAgentBuilder.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public <X> X build(
1515
String url,
1616
String outputKey,
1717
boolean async,
18+
boolean optional,
1819
String agentListenerName,
1920
Instance<Object> lookup) {
2021
// Defensive: createForA2A already validates the URL before calling this builder,

langchain4j-cdi-core/src/main/java/dev/langchain4j/cdi/agent/AgentAnnotationMeta.java

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import dev.langchain4j.cdi.spi.RegisterSimpleAgent;
1414
import dev.langchain4j.cdi.spi.RegisterSupervisorAgent;
1515
import java.lang.annotation.Annotation;
16+
import java.util.Arrays;
1617

1718
/**
1819
* Extracts CDI metadata from any of the 11 agent stereotype annotations: {@link RegisterSimpleAgent},
@@ -29,13 +30,26 @@
2930
* <p><b>API note:</b> this record is part of the stable public API of {@code langchain4j-cdi-core}, alongside the
3031
* annotations in {@code dev.langchain4j.cdi.spi}. It lives in the {@code dev.langchain4j.cdi.agent} package for
3132
* historical reasons; treat it as public and stable.
33+
*
34+
* @param scope the CDI scope annotation class (e.g. {@code ApplicationScoped.class})
35+
* @param rawName the agent name before expression resolution
36+
* @param rawDescription the agent description before expression resolution
37+
* @param rawOutputKey the output key before expression resolution
38+
* @param async whether the agent executes asynchronously
39+
* @param optional when {@code true} the agent's execution is silently skipped if any of its arguments is missing in the
40+
* agentic scope, instead of failing the entire agentic system
41+
* @param rawSummarizedContext names of other agents whose conversation context should be summarized and injected into
42+
* this agent's prompt, before expression resolution
43+
* @param annotationClass the concrete stereotype annotation class that was detected
3244
*/
3345
public record AgentAnnotationMeta(
3446
Class<? extends Annotation> scope,
3547
String rawName,
3648
String rawDescription,
3749
String rawOutputKey,
3850
boolean async,
51+
boolean optional,
52+
String[] rawSummarizedContext,
3953
Class<? extends Annotation> annotationClass) {
4054

4155
/**
@@ -61,17 +75,33 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
6175
simple.description(),
6276
simple.outputKey(),
6377
simple.async(),
78+
simple.optional(),
79+
simple.summarizedContext(),
6480
RegisterSimpleAgent.class);
6581

6682
RegisterSequenceAgent seq = interfaceClass.getAnnotation(RegisterSequenceAgent.class);
6783
if (seq != null)
6884
return new AgentAnnotationMeta(
69-
seq.scope(), seq.name(), seq.description(), seq.outputKey(), false, RegisterSequenceAgent.class);
85+
seq.scope(),
86+
seq.name(),
87+
seq.description(),
88+
seq.outputKey(),
89+
false,
90+
seq.optional(),
91+
seq.summarizedContext(),
92+
RegisterSequenceAgent.class);
7093

7194
RegisterLoopAgent loop = interfaceClass.getAnnotation(RegisterLoopAgent.class);
7295
if (loop != null)
7396
return new AgentAnnotationMeta(
74-
loop.scope(), loop.name(), loop.description(), loop.outputKey(), false, RegisterLoopAgent.class);
97+
loop.scope(),
98+
loop.name(),
99+
loop.description(),
100+
loop.outputKey(),
101+
false,
102+
loop.optional(),
103+
loop.summarizedContext(),
104+
RegisterLoopAgent.class);
75105

76106
RegisterParallelAgent parallel = interfaceClass.getAnnotation(RegisterParallelAgent.class);
77107
if (parallel != null)
@@ -81,6 +111,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
81111
parallel.description(),
82112
parallel.outputKey(),
83113
false,
114+
parallel.optional(),
115+
parallel.summarizedContext(),
84116
RegisterParallelAgent.class);
85117

86118
RegisterParallelMapperAgent mapper = interfaceClass.getAnnotation(RegisterParallelMapperAgent.class);
@@ -91,6 +123,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
91123
mapper.description(),
92124
mapper.outputKey(),
93125
false,
126+
mapper.optional(),
127+
mapper.summarizedContext(),
94128
RegisterParallelMapperAgent.class);
95129

96130
RegisterConditionalAgent cond = interfaceClass.getAnnotation(RegisterConditionalAgent.class);
@@ -101,6 +135,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
101135
cond.description(),
102136
cond.outputKey(),
103137
false,
138+
cond.optional(),
139+
cond.summarizedContext(),
104140
RegisterConditionalAgent.class);
105141

106142
RegisterSupervisorAgent supervisor = interfaceClass.getAnnotation(RegisterSupervisorAgent.class);
@@ -111,6 +147,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
111147
supervisor.description(),
112148
supervisor.outputKey(),
113149
false,
150+
supervisor.optional(),
151+
supervisor.summarizedContext(),
114152
RegisterSupervisorAgent.class);
115153

116154
RegisterPlannerAgent planner = interfaceClass.getAnnotation(RegisterPlannerAgent.class);
@@ -121,12 +159,21 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
121159
planner.description(),
122160
planner.outputKey(),
123161
false,
162+
planner.optional(),
163+
planner.summarizedContext(),
124164
RegisterPlannerAgent.class);
125165

126166
RegisterA2AAgent a2a = interfaceClass.getAnnotation(RegisterA2AAgent.class);
127167
if (a2a != null)
128168
return new AgentAnnotationMeta(
129-
a2a.scope(), a2a.name(), a2a.description(), a2a.outputKey(), a2a.async(), RegisterA2AAgent.class);
169+
a2a.scope(),
170+
a2a.name(),
171+
a2a.description(),
172+
a2a.outputKey(),
173+
a2a.async(),
174+
a2a.optional(),
175+
a2a.summarizedContext(),
176+
RegisterA2AAgent.class);
130177

131178
RegisterMcpClientAgent mcp = interfaceClass.getAnnotation(RegisterMcpClientAgent.class);
132179
if (mcp != null)
@@ -136,6 +183,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
136183
mcp.description(),
137184
mcp.outputKey(),
138185
mcp.async(),
186+
mcp.optional(),
187+
mcp.summarizedContext(),
139188
RegisterMcpClientAgent.class);
140189

141190
RegisterHumanInTheLoopAgent hitl = interfaceClass.getAnnotation(RegisterHumanInTheLoopAgent.class);
@@ -146,6 +195,8 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
146195
hitl.description(),
147196
hitl.outputKey(),
148197
hitl.async(),
198+
hitl.optional(),
199+
hitl.summarizedContext(),
149200
RegisterHumanInTheLoopAgent.class);
150201

151202
return null;
@@ -172,4 +223,17 @@ public String description() {
172223
public String outputKey() {
173224
return CdiLookupHelper.resolveExpression(rawOutputKey);
174225
}
226+
227+
/**
228+
* Expression-resolved summarized context agent names, equivalent to the annotation's {@code summarizedContext()}
229+
* after EL/Config expansion on each element.
230+
*/
231+
public String[] summarizedContext() {
232+
if (rawSummarizedContext == null || rawSummarizedContext.length == 0) {
233+
return new String[0];
234+
}
235+
return Arrays.stream(rawSummarizedContext)
236+
.map(CdiLookupHelper::resolveExpression)
237+
.toArray(String[]::new);
238+
}
175239
}

langchain4j-cdi-core/src/main/java/dev/langchain4j/cdi/agent/CommonAgentCreator.java

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
161161
builder.outputKey(resolvedOutputKey);
162162
}
163163
builder.async(ann.async());
164+
builder.optional(ann.optional());
165+
applySummarizedContext(builder::summarizedContext, ann.summarizedContext());
164166
applyListener(builder::listener, ann.agentListenerName(), lookup);
165167
},
166168
agentClass -> {
@@ -176,7 +178,7 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
176178
if (!hasText(outputKey)) outputKey = "";
177179
AgentListener listener = CdiLookupHelper.resolveSingle(lookup, AgentListener.class, ann.agentListenerName());
178180
NonAiAgentInstance agentInstance = buildNonAiAgentInstance(
179-
interfaceClass, entryMethod, ann.name(), description, outputKey, ann.async(), listener);
181+
interfaceClass, entryMethod, ann.name(), description, outputKey, ann.async(), ann.optional(), listener);
180182
InvocationHandler handler = (proxy, method, args) -> {
181183
Class<?> declaringClass = method.getDeclaringClass();
182184
if (declaringClass == Object.class) {
@@ -359,6 +361,7 @@ private static <X> X createForA2A(Instance<Object> lookup, Class<X> interfaceCla
359361
url,
360362
CdiLookupHelper.resolveExpression(ann.outputKey()),
361363
ann.async(),
364+
ann.optional(),
362365
ann.agentListenerName(),
363366
lookup);
364367
return wrapWithAgenticScope(interfaceClass, a2aAgent);
@@ -432,7 +435,7 @@ private static <X> X createForHumanInTheLoop(
432435
HumanInTheLoop spec = builder.build();
433436
Method entryMethod = findEntryMethod(interfaceClass);
434437
NonAiAgentInstance agentInstance = buildNonAiAgentInstance(
435-
interfaceClass, entryMethod, ann.name(), description, outputKey, ann.async(), listener);
438+
interfaceClass, entryMethod, ann.name(), description, outputKey, ann.async(), ann.optional(), listener);
436439
InvocationHandler handler = (proxy, method, args) -> {
437440
Class<?> declaringClass = method.getDeclaringClass();
438441
if (declaringClass == Object.class) {
@@ -494,12 +497,24 @@ private static NonAiAgentInstance buildNonAiAgentInstance(
494497
String description,
495498
String outputKey,
496499
boolean async,
500+
boolean optional,
497501
AgentListener listener) {
498502
String name = CdiLookupHelper.resolveExpression(rawName);
499503
if (!hasText(name)) {
500504
name = entryMethod != null ? entryMethod.getName() : interfaceClass.getSimpleName();
501505
}
502506
List<AgentArgument> arguments = entryMethod != null ? AgentUtil.argumentsFromMethod(entryMethod) : List.of();
507+
if (optional) {
508+
return new OptionalNonAiAgentInstance(
509+
interfaceClass,
510+
name,
511+
description,
512+
entryMethod != null ? entryMethod.getGenericReturnType() : String.class,
513+
outputKey,
514+
async,
515+
arguments,
516+
listener);
517+
}
503518
return new NonAiAgentInstance(
504519
interfaceClass,
505520
name,
@@ -511,6 +526,32 @@ private static NonAiAgentInstance buildNonAiAgentInstance(
511526
listener);
512527
}
513528

529+
/**
530+
* Extends {@link NonAiAgentInstance} to override {@link #optional()} so that non-AI agent proxies can report
531+
* themselves as optional. The upstream class does not expose an {@code optional} field; its
532+
* {@link dev.langchain4j.agentic.planner.AgentInstance#optional() AgentInstance.optional()} default returns
533+
* {@code false}.
534+
*/
535+
private static class OptionalNonAiAgentInstance extends NonAiAgentInstance {
536+
537+
OptionalNonAiAgentInstance(
538+
Class<?> type,
539+
String name,
540+
String description,
541+
java.lang.reflect.Type outputType,
542+
String outputKey,
543+
boolean async,
544+
List<AgentArgument> arguments,
545+
AgentListener listener) {
546+
super(type, name, description, outputType, outputKey, async, arguments, listener);
547+
}
548+
549+
@Override
550+
public boolean optional() {
551+
return true;
552+
}
553+
}
554+
514555
@SuppressWarnings("unchecked")
515556
private static <X, S extends AgenticService<S, X>> X buildComposed(
516557
S builder,
@@ -568,6 +609,19 @@ private static void applyListener(
568609
}
569610
}
570611

612+
private static void applySummarizedContext(Consumer<String[]> setter, String[] rawSummarizedContext) {
613+
if (rawSummarizedContext == null || rawSummarizedContext.length == 0) {
614+
return;
615+
}
616+
String[] resolved = Arrays.stream(rawSummarizedContext)
617+
.map(CdiLookupHelper::resolveExpression)
618+
.filter(s -> hasText(s))
619+
.toArray(String[]::new);
620+
if (resolved.length > 0) {
621+
setter.accept(resolved);
622+
}
623+
}
624+
571625
// =========================================================================
572626
// AgentComponents — resolves dependencies for @RegisterSimpleAgent
573627
// =========================================================================
@@ -887,7 +941,16 @@ private static AgentExecutor tryBuildExecutor(Object bean, InternalAgent ia, Cla
887941
String desc = meta.description();
888942
String outputKey = meta.outputKey();
889943
boolean async = meta.async();
890-
AgentInvoker invoker = AgentUtil.nonAiAgentInvoker(entryMethod, name, desc, outputKey, async);
944+
boolean optional = meta.optional();
945+
AgentInvoker invoker;
946+
if (optional) {
947+
List<AgentArgument> arguments = AgentUtil.argumentsFromMethod(entryMethod);
948+
NonAiAgentInstance agent = new OptionalNonAiAgentInstance(
949+
iface, name, desc, entryMethod.getGenericReturnType(), outputKey, async, arguments, null);
950+
invoker = AgentInvoker.fromMethod(agent, entryMethod);
951+
} else {
952+
invoker = AgentUtil.nonAiAgentInvoker(entryMethod, name, desc, outputKey, async);
953+
}
891954
return new AgentExecutor(invoker, ia);
892955
}
893956

langchain4j-cdi-core/src/main/java/dev/langchain4j/cdi/agent/spi/A2AAgentBuilder.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,53 @@
88
*/
99
public interface A2AAgentBuilder {
1010

11-
/** Build an A2A agent from individual field values. */
11+
/**
12+
* Build an A2A agent from individual field values.
13+
*
14+
* <p>Delegates to {@link #build(Class, String, String, boolean, boolean, String, Instance)} with {@code optional}
15+
* set to {@code false}.
16+
*
17+
* @param <X> the agent interface type
18+
* @param interfaceClass the agent interface class
19+
* @param url the A2A server URL
20+
* @param outputKey the key under which the agent stores its output in the agentic scope, or blank to skip
21+
* @param async whether the agent executes asynchronously
22+
* @param agentListenerName CDI bean name of the {@link dev.langchain4j.agentic.observability.AgentListener}, or
23+
* blank for none
24+
* @param lookup CDI lookup instance for resolving named beans
25+
* @return a proxy implementing {@code interfaceClass} that delegates to the remote A2A server
26+
*/
27+
default <X> X build(
28+
Class<X> interfaceClass,
29+
String url,
30+
String outputKey,
31+
boolean async,
32+
String agentListenerName,
33+
Instance<Object> lookup) {
34+
return build(interfaceClass, url, outputKey, async, false, agentListenerName, lookup);
35+
}
36+
37+
/**
38+
* Build an A2A agent from individual field values including the optional flag.
39+
*
40+
* @param <X> the agent interface type
41+
* @param interfaceClass the agent interface class
42+
* @param url the A2A server URL
43+
* @param outputKey the key under which the agent stores its output in the agentic scope, or blank to skip
44+
* @param async whether the agent executes asynchronously
45+
* @param optional when {@code true} the agent's execution is silently skipped if any of its arguments is missing in
46+
* the agentic scope, instead of failing the entire agentic system
47+
* @param agentListenerName CDI bean name of the {@link dev.langchain4j.agentic.observability.AgentListener}, or
48+
* blank for none
49+
* @param lookup CDI lookup instance for resolving named beans
50+
* @return a proxy implementing {@code interfaceClass} that delegates to the remote A2A server
51+
*/
1252
<X> X build(
1353
Class<X> interfaceClass,
1454
String url,
1555
String outputKey,
1656
boolean async,
57+
boolean optional,
1758
String agentListenerName,
1859
Instance<Object> lookup);
1960
}

langchain4j-cdi-core/src/main/java/dev/langchain4j/cdi/spi/RegisterA2AAgent.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@
3131

3232
boolean async() default false;
3333

34+
/**
35+
* If true, the agent's execution will be silently skipped when any of its arguments is missing in the agentic
36+
* scope, instead of making the agentic system's execution fail.
37+
*/
38+
boolean optional() default false;
39+
40+
/** Names of other agents whose conversation context should be summarized and injected into this agent's prompt. */
41+
String[] summarizedContext() default {};
42+
3443
String agentListenerName() default "";
3544

3645
/** URL of the A2A server. Required. */

langchain4j-cdi-core/src/main/java/dev/langchain4j/cdi/spi/RegisterAIService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
import java.lang.annotation.Retention;
1212
import java.lang.annotation.Target;
1313

14-
@Retention(RUNTIME)
15-
@Target(ElementType.TYPE)
16-
@Stereotype
1714
/**
1815
* Stereotype to register an interface as a LangChain4j AI Service.
1916
*
2017
* <p>Apply it on an interface that will be implemented dynamically by LangChain4j AiServices. You can optionally
2118
* reference named CDI beans to wire the service: models, retrievers, tools, memories, etc. If a property name is blank,
2219
* the dependency is ignored. For chatModelName, "#default" means select the default bean.
2320
*/
21+
@Retention(RUNTIME)
22+
@Target(ElementType.TYPE)
23+
@Stereotype
2424
public @interface RegisterAIService {
2525

2626
Class<? extends Annotation> scope() default RequestScoped.class;

0 commit comments

Comments
 (0)