Skip to content

Commit 286cdb9

Browse files
committed
CCE on agentic loop call after agent call
Signed-off-by: Dmitrii Tikhomirov <[email protected]>
1 parent 044a07f commit 286cdb9

File tree

9 files changed

+352
-4
lines changed

9 files changed

+352
-4
lines changed

experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717

1818
import dev.langchain4j.agentic.scope.AgenticScope;
1919
import io.serverlessworkflow.impl.WorkflowModel;
20+
import io.serverlessworkflow.impl.expressions.func.HasAgenticScope;
2021
import io.serverlessworkflow.impl.expressions.func.JavaModel;
2122
import java.util.Collection;
2223
import java.util.Map;
2324
import java.util.Optional;
2425

25-
class AgenticModel extends JavaModel {
26+
class AgenticModel extends JavaModel implements HasAgenticScope<AgenticScope> {
2627

2728
private final AgenticScope agenticScope;
2829

@@ -31,6 +32,7 @@ class AgenticModel extends JavaModel {
3132
this.agenticScope = agenticScope;
3233
}
3334

35+
@Override
3436
public AgenticScope getAgenticScope() {
3537
return agenticScope;
3638
}

experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public WorkflowModel fromAny(WorkflowModel prev, Object obj) {
5555
// hood, the agent already updated it.
5656
if (prev instanceof AgenticModel agenticModel) {
5757
this.scopeRegistryAssessor.setAgenticScope(agenticModel.getAgenticScope());
58+
agenticModel.getAgenticScope().state().put(DEFAULT_AGENTIC_SCOPE_STATE_KEY, obj);
5859
}
5960
return newAgenticModel(obj);
6061
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
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 io.serverlessworkflow.fluent.agentic.langchain4j;
17+
18+
import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow;
19+
import static io.serverlessworkflow.fluent.agentic.Agents.*;
20+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.*;
21+
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.*;
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
import static org.junit.jupiter.api.Assertions.assertNotNull;
24+
25+
import dev.langchain4j.agentic.scope.AgenticScope;
26+
import io.serverlessworkflow.fluent.agentic.AgenticServices;
27+
import io.serverlessworkflow.fluent.agentic.AgentsUtils;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.function.Function;
31+
import java.util.function.Predicate;
32+
import java.util.stream.IntStream;
33+
import org.junit.jupiter.api.Test;
34+
35+
public class AgenticServicesHelperTest {
36+
37+
@Test
38+
public void sequenceHelperTest() {
39+
var creativeWriter = newCreativeWriter();
40+
var audienceEditor = newAudienceEditor();
41+
var styleEditor = newStyleEditor();
42+
43+
NovelCreator novelCreator =
44+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(NovelCreator.class)
45+
.flow(workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor))
46+
.build();
47+
48+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
49+
assertNotNull(story);
50+
}
51+
52+
@Test
53+
public void agentAndSequenceHelperTest() {
54+
var creativeWriter = newCreativeWriter();
55+
var audienceEditor = newAudienceEditor();
56+
var styleEditor = newStyleEditor();
57+
58+
NovelCreator novelCreator =
59+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(NovelCreator.class)
60+
.flow(workflow("seqFlow").agent(creativeWriter).sequence(audienceEditor, styleEditor))
61+
.build();
62+
63+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
64+
assertNotNull(story);
65+
}
66+
67+
@Test
68+
public void agentAndSequenceAndAgentHelperTest() {
69+
var creativeWriter = newCreativeWriter();
70+
var audienceEditor = newAudienceEditor();
71+
var styleEditor = newStyleEditor();
72+
var summaryStory = newSummaryStory();
73+
74+
NovelCreator novelCreator =
75+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(NovelCreator.class)
76+
.flow(
77+
workflow("seqFlow")
78+
.agent(creativeWriter)
79+
.sequence(audienceEditor, styleEditor)
80+
.agent(summaryStory))
81+
.build();
82+
83+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
84+
assertNotNull(story);
85+
}
86+
87+
@Test
88+
public void parallelWorkflow() {
89+
var foodExpert = newFoodExpert();
90+
var movieExpert = newMovieExpert();
91+
92+
Function<Map<String, List<String>>, List<EveningPlan>> planEvening =
93+
input -> {
94+
List<String> movies = input.get("findMovie");
95+
List<String> meals = input.get("findMeal");
96+
97+
int max = Math.min(movies.size(), meals.size());
98+
return IntStream.range(0, max)
99+
.mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i)))
100+
.toList();
101+
};
102+
103+
EveningPlannerAgent eveningPlannerAgent =
104+
AgenticServices.of(EveningPlannerAgent.class)
105+
.flow(workflow("parallelFlow").parallel(foodExpert, movieExpert).outputAs(planEvening))
106+
.build();
107+
List<EveningPlan> result = eveningPlannerAgent.plan("romantic");
108+
assertEquals(3, result.size());
109+
}
110+
111+
@Test
112+
public void loopTest() {
113+
var creativeWriter = AgentsUtils.newCreativeWriter();
114+
var scorer = AgentsUtils.newStyleScorer();
115+
var editor = AgentsUtils.newStyleEditor();
116+
117+
Predicate<AgenticScope> until = s -> s.readState("score", 0.0) >= 0.8;
118+
119+
StyledWriter styledWriter =
120+
AgenticServices.of(StyledWriter.class)
121+
.flow(workflow("loopFlow").agent(creativeWriter).loop(until, scorer, editor))
122+
.build();
123+
124+
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "fantasy");
125+
assertNotNull(story);
126+
}
127+
128+
@Test
129+
public void humanInTheLoop() {
130+
var astrologyAgent = newAstrologyAgent();
131+
132+
var askSign =
133+
new Function<Map<String, Object>, Map<String, Object>>() {
134+
@Override
135+
public Map<String, Object> apply(Map<String, Object> holder) {
136+
System.out.println("What's your star sign?");
137+
// var sign = System.console().readLine();
138+
holder.put("sign", "piscis");
139+
return holder;
140+
}
141+
};
142+
143+
String result =
144+
AgenticServices.of(HoroscopeAgent.class)
145+
.flow(
146+
workflow("humanInTheLoop")
147+
.inputFrom(askSign)
148+
// .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too
149+
.agent(astrologyAgent))
150+
.build()
151+
.invoke("My name is Mario. What is my horoscope?");
152+
153+
assertNotNull(result);
154+
}
155+
}

experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import dev.langchain4j.agent.tool.Tool;
1919
import dev.langchain4j.agentic.Agent;
2020
import dev.langchain4j.agentic.scope.AgenticScopeAccess;
21-
import dev.langchain4j.agentic.scope.ResultWithAgenticScope;
2221
import dev.langchain4j.service.MemoryId;
2322
import dev.langchain4j.service.UserMessage;
2423
import dev.langchain4j.service.V;
@@ -212,8 +211,7 @@ public interface StyleReviewLoop {
212211
public interface StyledWriter extends AgenticScopeAccess {
213212

214213
@Agent
215-
ResultWithAgenticScope<String> writeStoryWithStyle(
216-
@V("topic") String topic, @V("style") String style);
214+
String writeStoryWithStyle(@V("topic") String topic, @V("style") String style);
217215
}
218216

219217
public interface FoodExpert {
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
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 io.serverlessworkflow.fluent.agentic;
17+
18+
import dev.langchain4j.agentic.Agent;
19+
import dev.langchain4j.agentic.scope.AgenticScope;
20+
import dev.langchain4j.service.V;
21+
import io.serverlessworkflow.api.types.Workflow;
22+
import io.serverlessworkflow.impl.WorkflowApplication;
23+
import io.serverlessworkflow.impl.WorkflowModel;
24+
import java.lang.annotation.Annotation;
25+
import java.lang.reflect.InvocationHandler;
26+
import java.lang.reflect.Method;
27+
import java.lang.reflect.Proxy;
28+
import java.util.HashMap;
29+
import java.util.Map;
30+
import java.util.Objects;
31+
32+
public class AgenticServices<T> {
33+
34+
private final Class<T> agent;
35+
36+
private AgentWorkflowBuilder builder;
37+
38+
private AgenticServices(Class<T> agent) {
39+
this.agent = agent;
40+
}
41+
42+
public static <T> AgenticServices<T> of(Class<T> agent) {
43+
return new AgenticServices<>(agent);
44+
}
45+
46+
public AgenticServices<T> flow(AgentWorkflowBuilder builder) {
47+
this.builder = builder;
48+
return this;
49+
}
50+
51+
public T build() {
52+
Objects.requireNonNull(
53+
builder, "AgenticServices.flow(AgentWorkflowBuilder) must be called before build()");
54+
Workflow workflow = builder.build();
55+
return AgenticServiceBuilder.create(agent, new AgentInvocationHandler(workflow));
56+
}
57+
58+
private static class AgenticServiceBuilder {
59+
60+
@SuppressWarnings("unchecked")
61+
public static <T> T create(Class<T> runner, InvocationHandler h) {
62+
if (!runner.isInterface()) {
63+
throw new IllegalArgumentException(runner + " must be an interface to create a Proxy");
64+
}
65+
66+
ClassLoader cl = runner.getClassLoader();
67+
Class<?>[] ifaces = new Class<?>[] {runner};
68+
return (T) Proxy.newProxyInstance(cl, ifaces, h);
69+
}
70+
}
71+
72+
private class AgentInvocationHandler implements InvocationHandler {
73+
74+
private final Workflow workflow;
75+
76+
public AgentInvocationHandler(Workflow workflow) {
77+
this.workflow = workflow;
78+
}
79+
80+
@Override
81+
public Object invoke(Object proxy, Method method, Object[] args) {
82+
if (method.getDeclaringClass() == Object.class) {
83+
return switch (method.getName()) {
84+
case "toString" -> "AgentProxy(" + workflow.getDocument().getName() + ")";
85+
case "hashCode" -> System.identityHashCode(proxy);
86+
case "equals" -> proxy == args[0];
87+
default -> throw new IllegalStateException("Unexpected Object method: " + method);
88+
};
89+
}
90+
91+
Agent agent = method.getAnnotation(Agent.class);
92+
if (agent == null) {
93+
throw new IllegalStateException(
94+
"Method " + method.getName() + " is not annotated with @Agent");
95+
}
96+
97+
Annotation[][] annotations = method.getParameterAnnotations();
98+
Map<String, Object> input = new HashMap<>();
99+
for (int i = 0; i < annotations.length; i++) {
100+
boolean found = false;
101+
for (Annotation a : annotations[i]) {
102+
if (a instanceof V) {
103+
String key = ((V) a).value();
104+
Object value = args[i];
105+
input.put(key, value);
106+
found = true;
107+
break;
108+
}
109+
}
110+
if (!found) {
111+
throw new IllegalStateException(
112+
"Parameter "
113+
+ (i + 1)
114+
+ " of method "
115+
+ method.getName()
116+
+ " is not annotated with @V");
117+
}
118+
}
119+
120+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
121+
WorkflowModel result = app.workflowDefinition(workflow).instance(input).start().get();
122+
if (result.asJavaObject() instanceof AgenticScope scope) {
123+
Object out = scope.state().get("input");
124+
if (out != null) {
125+
return out;
126+
}
127+
}
128+
return result.asJavaObject();
129+
} catch (Exception e) {
130+
throw new RuntimeException("Workflow execution failed", e);
131+
}
132+
}
133+
}
134+
}

experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,16 @@ interface StyleScorer {
293293
double scoreStyle(@V("story") String story, @V("style") String style);
294294
}
295295

296+
interface SummaryStory {
297+
298+
@UserMessage(
299+
"""
300+
Return only the summary text and nothing else.
301+
""")
302+
@Agent("Edits a story to better fit a given audience")
303+
String summaryStory(@V("story") String story);
304+
}
305+
296306
interface FoodExpert {
297307

298308
@UserMessage(
@@ -320,6 +330,12 @@ interface AstrologyAgent {
320330
String horoscope(@V("name") String name, @V("sign") String sign);
321331
}
322332

333+
interface HoroscopeAgent {
334+
335+
@Agent
336+
String invoke(@V("name") String name);
337+
}
338+
323339
enum RequestCategory {
324340
LEGAL,
325341
MEDICAL,

0 commit comments

Comments
 (0)