Skip to content

Commit bc22dcb

Browse files
authored
Merge pull request #226 from ehsavoie/typedKey
feature: Add typedOutputKey attribute to all agent annotations
2 parents 6808b6c + 2f9e59b commit bc22dcb

15 files changed

Lines changed: 343 additions & 19 deletions

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dev.langchain4j.cdi.agent;
22

3+
import dev.langchain4j.agentic.Agent;
4+
import dev.langchain4j.agentic.declarative.TypedKey;
35
import dev.langchain4j.cdi.aiservice.CdiLookupHelper;
46
import dev.langchain4j.cdi.spi.RegisterA2AAgent;
57
import dev.langchain4j.cdi.spi.RegisterConditionalAgent;
@@ -35,6 +37,7 @@
3537
* @param rawName the agent name before expression resolution
3638
* @param rawDescription the agent description before expression resolution
3739
* @param rawOutputKey the output key before expression resolution
40+
* @param rawTypedOutputKey the typed output key class, or {@code Agent.NoTypedKey.class} when not set
3841
* @param async whether the agent executes asynchronously
3942
* @param optional when {@code true} the agent's execution is silently skipped if any of its arguments is missing in the
4043
* agentic scope, instead of failing the entire agentic system
@@ -47,6 +50,7 @@ public record AgentAnnotationMeta(
4750
String rawName,
4851
String rawDescription,
4952
String rawOutputKey,
53+
Class<? extends TypedKey<?>> rawTypedOutputKey,
5054
boolean async,
5155
boolean optional,
5256
String[] rawSummarizedContext,
@@ -74,6 +78,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
7478
simple.name(),
7579
simple.description(),
7680
simple.outputKey(),
81+
simple.typedOutputKey(),
7782
simple.async(),
7883
simple.optional(),
7984
simple.summarizedContext(),
@@ -86,6 +91,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
8691
seq.name(),
8792
seq.description(),
8893
seq.outputKey(),
94+
seq.typedOutputKey(),
8995
false,
9096
seq.optional(),
9197
seq.summarizedContext(),
@@ -98,6 +104,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
98104
loop.name(),
99105
loop.description(),
100106
loop.outputKey(),
107+
loop.typedOutputKey(),
101108
false,
102109
loop.optional(),
103110
loop.summarizedContext(),
@@ -110,6 +117,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
110117
parallel.name(),
111118
parallel.description(),
112119
parallel.outputKey(),
120+
parallel.typedOutputKey(),
113121
false,
114122
parallel.optional(),
115123
parallel.summarizedContext(),
@@ -122,6 +130,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
122130
mapper.name(),
123131
mapper.description(),
124132
mapper.outputKey(),
133+
mapper.typedOutputKey(),
125134
false,
126135
mapper.optional(),
127136
mapper.summarizedContext(),
@@ -134,6 +143,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
134143
cond.name(),
135144
cond.description(),
136145
cond.outputKey(),
146+
cond.typedOutputKey(),
137147
false,
138148
cond.optional(),
139149
cond.summarizedContext(),
@@ -146,6 +156,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
146156
supervisor.name(),
147157
supervisor.description(),
148158
supervisor.outputKey(),
159+
supervisor.typedOutputKey(),
149160
false,
150161
supervisor.optional(),
151162
supervisor.summarizedContext(),
@@ -158,6 +169,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
158169
planner.name(),
159170
planner.description(),
160171
planner.outputKey(),
172+
planner.typedOutputKey(),
161173
false,
162174
planner.optional(),
163175
planner.summarizedContext(),
@@ -170,6 +182,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
170182
a2a.name(),
171183
a2a.description(),
172184
a2a.outputKey(),
185+
a2a.typedOutputKey(),
173186
a2a.async(),
174187
a2a.optional(),
175188
a2a.summarizedContext(),
@@ -182,6 +195,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
182195
mcp.name(),
183196
mcp.description(),
184197
mcp.outputKey(),
198+
mcp.typedOutputKey(),
185199
mcp.async(),
186200
mcp.optional(),
187201
mcp.summarizedContext(),
@@ -194,6 +208,7 @@ public static AgentAnnotationMeta detect(Class<?> interfaceClass) {
194208
hitl.name(),
195209
hitl.description(),
196210
hitl.outputKey(),
211+
hitl.typedOutputKey(),
197212
hitl.async(),
198213
hitl.optional(),
199214
hitl.summarizedContext(),
@@ -224,6 +239,11 @@ public String outputKey() {
224239
return CdiLookupHelper.resolveExpression(rawOutputKey);
225240
}
226241

242+
/** Returns {@code true} when a typed output key class other than {@link Agent.NoTypedKey} is set. */
243+
public boolean hasTypedOutputKey() {
244+
return rawTypedOutputKey != null && rawTypedOutputKey != Agent.NoTypedKey.class;
245+
}
246+
227247
/**
228248
* Expression-resolved summarized context agent names, equivalent to the annotation's {@code summarizedContext()}
229249
* after EL/Config expansion on each element.

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

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import dev.langchain4j.agentic.AgenticServices;
77
import dev.langchain4j.agentic.agent.AgentBuilder;
88
import dev.langchain4j.agentic.declarative.PlannerSupplier;
9+
import dev.langchain4j.agentic.declarative.TypedKey;
910
import dev.langchain4j.agentic.internal.AgentExecutor;
1011
import dev.langchain4j.agentic.internal.AgentInvoker;
1112
import dev.langchain4j.agentic.internal.AgentUtil;
@@ -156,9 +157,14 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
156157
if (hasText(resolvedDescription)) {
157158
builder.description(resolvedDescription);
158159
}
159-
String resolvedOutputKey = CdiLookupHelper.resolveExpression(ann.outputKey());
160-
if (hasText(resolvedOutputKey)) {
161-
builder.outputKey(resolvedOutputKey);
160+
if (ann.typedOutputKey() != Agent.NoTypedKey.class) {
161+
warnIfBothOutputKeysSet(ann.outputKey(), ann.typedOutputKey());
162+
builder.outputKey(ann.typedOutputKey());
163+
} else {
164+
String resolvedOutputKey = CdiLookupHelper.resolveExpression(ann.outputKey());
165+
if (hasText(resolvedOutputKey)) {
166+
builder.outputKey(resolvedOutputKey);
167+
}
162168
}
163169
builder.async(ann.async());
164170
builder.optional(ann.optional());
@@ -174,7 +180,7 @@ private static <X> X createForSimple(Instance<Object> lookup, Class<X> interface
174180
X aiService = buildAiServiceForSimple(interfaceClass, ann, chatModel, streamingChatModel, lookup);
175181
String description = CdiLookupHelper.resolveExpression(ann.description());
176182
if (!hasText(description)) description = "";
177-
String outputKey = CdiLookupHelper.resolveExpression(ann.outputKey());
183+
String outputKey = resolveOutputKey(ann.typedOutputKey(), ann.outputKey());
178184
if (!hasText(outputKey)) outputKey = "";
179185
AgentListener listener = CdiLookupHelper.resolveSingle(lookup, AgentListener.class, ann.agentListenerName());
180186
NonAiAgentInstance agentInstance = buildNonAiAgentInstance(
@@ -231,6 +237,7 @@ private static <X> X createForSequence(
231237
ann.name(),
232238
ann.description(),
233239
ann.outputKey(),
240+
ann.typedOutputKey(),
234241
ann.subAgentNames(),
235242
ann.agentListenerName(),
236243
lookup);
@@ -250,6 +257,7 @@ private static <X> X createForLoop(Instance<Object> lookup, Class<X> interfaceCl
250257
ann.name(),
251258
ann.description(),
252259
ann.outputKey(),
260+
ann.typedOutputKey(),
253261
ann.subAgentNames(),
254262
ann.agentListenerName(),
255263
lookup);
@@ -262,6 +270,7 @@ private static <X> X createForParallel(
262270
ann.name(),
263271
ann.description(),
264272
ann.outputKey(),
273+
ann.typedOutputKey(),
265274
ann.subAgentNames(),
266275
ann.agentListenerName(),
267276
lookup);
@@ -281,6 +290,7 @@ private static <X> X createForParallelMapper(
281290
ann.name(),
282291
ann.description(),
283292
ann.outputKey(),
293+
ann.typedOutputKey(),
284294
ann.subAgentNames(),
285295
ann.agentListenerName(),
286296
lookup);
@@ -293,6 +303,7 @@ private static <X> X createForConditional(
293303
ann.name(),
294304
ann.description(),
295305
ann.outputKey(),
306+
ann.typedOutputKey(),
296307
ann.subAgentNames(),
297308
ann.agentListenerName(),
298309
lookup);
@@ -318,14 +329,17 @@ private static <X> X createForSupervisor(
318329
builder.supervisorContext(supervisorContext);
319330
}
320331
List<Object> subAgents = resolveSubAgents(lookup, ann.subAgentNames());
332+
String resolvedOutputKey = resolveOutputKey(ann.typedOutputKey(), ann.outputKey());
321333
configureCommonFields(
322334
builder::subAgents,
323335
builder::name,
324336
builder::description,
325337
builder::outputKey,
338+
null,
326339
ann.name(),
327340
ann.description(),
328-
ann.outputKey(),
341+
resolvedOutputKey,
342+
Agent.NoTypedKey.class,
329343
subAgents);
330344
applyListener(builder::listener, ann.agentListenerName(), lookup);
331345
return builder.build();
@@ -344,6 +358,7 @@ private static <X> X createForPlanner(Instance<Object> lookup, Class<X> interfac
344358
ann.name(),
345359
ann.description(),
346360
ann.outputKey(),
361+
ann.typedOutputKey(),
347362
ann.subAgentNames(),
348363
ann.agentListenerName(),
349364
lookup);
@@ -355,15 +370,9 @@ private static <X> X createForA2A(Instance<Object> lookup, Class<X> interfaceCla
355370
throw new IllegalArgumentException(
356371
"@RegisterA2AAgent on " + interfaceClass.getSimpleName() + ": 'a2aServerUrl' is required.");
357372
}
373+
String outputKey = resolveOutputKey(ann.typedOutputKey(), ann.outputKey());
358374
X a2aAgent = loadA2ABuilder()
359-
.build(
360-
interfaceClass,
361-
url,
362-
CdiLookupHelper.resolveExpression(ann.outputKey()),
363-
ann.async(),
364-
ann.optional(),
365-
ann.agentListenerName(),
366-
lookup);
375+
.build(interfaceClass, url, outputKey, ann.async(), ann.optional(), ann.agentListenerName(), lookup);
367376
return wrapWithAgenticScope(interfaceClass, a2aAgent);
368377
}
369378

@@ -390,7 +399,7 @@ private static <X> X createForMcpClient(
390399
if (ann.mcpInputKeys().length > 0) {
391400
builder.inputKeys(ann.mcpInputKeys());
392401
}
393-
String outputKey = CdiLookupHelper.resolveExpression(ann.outputKey());
402+
String outputKey = resolveOutputKey(ann.typedOutputKey(), ann.outputKey());
394403
if (hasText(outputKey)) {
395404
builder.outputKey(outputKey);
396405
}
@@ -419,7 +428,7 @@ private static <X> X createForHumanInTheLoop(
419428
throw new RuntimeException(cause);
420429
}
421430
});
422-
String outputKey = CdiLookupHelper.resolveExpression(ann.outputKey());
431+
String outputKey = resolveOutputKey(ann.typedOutputKey(), ann.outputKey());
423432
if (hasText(outputKey)) {
424433
builder.outputKey(outputKey);
425434
}
@@ -558,6 +567,7 @@ private static <X, S extends AgenticService<S, X>> X buildComposed(
558567
String name,
559568
String description,
560569
String outputKey,
570+
Class<? extends TypedKey<?>> typedOutputKey,
561571
String[] subAgentNames,
562572
String agentListenerName,
563573
Instance<Object> lookup) {
@@ -567,9 +577,11 @@ private static <X, S extends AgenticService<S, X>> X buildComposed(
567577
builder::name,
568578
builder::description,
569579
builder::outputKey,
580+
typedOutputKey != Agent.NoTypedKey.class ? builder::outputKey : null,
570581
name,
571582
description,
572583
outputKey,
584+
typedOutputKey,
573585
subAgents);
574586
applyListener(builder::listener, agentListenerName, lookup);
575587
return builder.build();
@@ -580,9 +592,11 @@ private static void configureCommonFields(
580592
Consumer<String> nameSetter,
581593
Consumer<String> descriptionSetter,
582594
Consumer<String> outputKeySetter,
595+
Consumer<Class<? extends TypedKey<?>>> typedOutputKeySetter,
583596
String rawName,
584597
String rawDescription,
585598
String rawOutputKey,
599+
Class<? extends TypedKey<?>> typedOutputKey,
586600
List<Object> subAgents) {
587601
if (!subAgents.isEmpty()) {
588602
subAgentsSetter.accept(subAgents);
@@ -595,9 +609,53 @@ private static void configureCommonFields(
595609
if (hasText(description)) {
596610
descriptionSetter.accept(description);
597611
}
598-
String outputKey = CdiLookupHelper.resolveExpression(rawOutputKey);
599-
if (hasText(outputKey)) {
600-
outputKeySetter.accept(outputKey);
612+
if (typedOutputKey != null && typedOutputKey != Agent.NoTypedKey.class) {
613+
warnIfBothOutputKeysSet(rawOutputKey, typedOutputKey);
614+
if (typedOutputKeySetter != null) {
615+
typedOutputKeySetter.accept(typedOutputKey);
616+
} else {
617+
String resolved = resolveTypedKeyName(typedOutputKey);
618+
if (hasText(resolved)) {
619+
outputKeySetter.accept(resolved);
620+
}
621+
}
622+
} else {
623+
String outputKey = CdiLookupHelper.resolveExpression(rawOutputKey);
624+
if (hasText(outputKey)) {
625+
outputKeySetter.accept(outputKey);
626+
}
627+
}
628+
}
629+
630+
static String resolveOutputKey(Class<? extends TypedKey<?>> typedOutputKey, String rawOutputKey) {
631+
if (typedOutputKey != null && typedOutputKey != Agent.NoTypedKey.class) {
632+
warnIfBothOutputKeysSet(rawOutputKey, typedOutputKey);
633+
return resolveTypedKeyName(typedOutputKey);
634+
}
635+
return CdiLookupHelper.resolveExpression(rawOutputKey);
636+
}
637+
638+
private static void warnIfBothOutputKeysSet(String rawOutputKey, Class<? extends TypedKey<?>> typedOutputKey) {
639+
if (hasText(rawOutputKey)) {
640+
LOGGER.log(
641+
Level.WARNING,
642+
"Both outputKey (''{0}'') and typedOutputKey ({1}) are set; typedOutputKey takes precedence",
643+
new Object[] {rawOutputKey, typedOutputKey.getName()});
644+
}
645+
}
646+
647+
static String resolveTypedKeyName(Class<? extends TypedKey<?>> typedKeyClass) {
648+
try {
649+
TypedKey<?> instance = typedKeyClass.getDeclaredConstructor().newInstance();
650+
String name = instance.name();
651+
return hasText(name) ? name : typedKeyClass.getSimpleName();
652+
} catch (Exception e) {
653+
LOGGER.log(
654+
Level.WARNING,
655+
e,
656+
() -> "Cannot instantiate TypedKey class " + typedKeyClass.getName()
657+
+ " (no accessible no-arg constructor?), falling back to simple class name");
658+
return typedKeyClass.getSimpleName();
601659
}
602660
}
603661

@@ -939,7 +997,7 @@ private static AgentExecutor tryBuildExecutor(Object bean, InternalAgent ia, Cla
939997
String resolvedName = meta.name();
940998
String name = hasText(resolvedName) ? resolvedName : entryMethod.getName();
941999
String desc = meta.description();
942-
String outputKey = meta.outputKey();
1000+
String outputKey = resolveOutputKey(meta.rawTypedOutputKey(), meta.rawOutputKey());
9431001
boolean async = meta.async();
9441002
boolean optional = meta.optional();
9451003
AgentInvoker invoker;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static java.lang.annotation.RetentionPolicy.RUNTIME;
44

5+
import dev.langchain4j.agentic.Agent;
6+
import dev.langchain4j.agentic.declarative.TypedKey;
57
import jakarta.enterprise.context.ApplicationScoped;
68
import jakarta.enterprise.inject.Stereotype;
79
import java.lang.annotation.Annotation;
@@ -29,6 +31,8 @@
2931

3032
String outputKey() default "";
3133

34+
Class<? extends TypedKey<?>> typedOutputKey() default Agent.NoTypedKey.class;
35+
3236
boolean async() default false;
3337

3438
/**

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static java.lang.annotation.RetentionPolicy.RUNTIME;
44

5+
import dev.langchain4j.agentic.Agent;
6+
import dev.langchain4j.agentic.declarative.TypedKey;
57
import jakarta.enterprise.context.ApplicationScoped;
68
import jakarta.enterprise.inject.Stereotype;
79
import java.lang.annotation.Annotation;
@@ -25,6 +27,8 @@
2527

2628
String outputKey() default "";
2729

30+
Class<? extends TypedKey<?>> typedOutputKey() default Agent.NoTypedKey.class;
31+
2832
/**
2933
* If true, the agent's execution will be silently skipped when any of its arguments is missing in the agentic
3034
* scope, instead of making the agentic system's execution fail.

0 commit comments

Comments
 (0)