Skip to content

Commit 9d6980e

Browse files
committed
Added AgenticServices helper
Signed-off-by: Dmitrii Tikhomirov <[email protected]>
1 parent 666cdb3 commit 9d6980e

File tree

4 files changed

+225
-8
lines changed

4 files changed

+225
-8
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,13 @@ public interface StyleReviewLoop {
212212
public interface StyledWriter extends AgenticScopeAccess {
213213

214214
@Agent
215-
ResultWithAgenticScope<String> writeStoryWithStyle(
216-
@V("topic") String topic, @V("style") String style);
215+
String writeStoryWithStyle(@V("topic") String topic, @V("style") String style);
216+
}
217+
218+
public interface NovelCreator {
219+
220+
@Agent
221+
String createNovel(@V("topic") String topic, @V("audience") String audience, @V("style") String style);
217222
}
218223

219224
public interface FoodExpert {
@@ -250,4 +255,11 @@ public interface EveningPlannerAgent {
250255
@Agent
251256
List<EveningPlan> plan(@V("mood") String mood);
252257
}
258+
259+
public interface HoroscopeAgent {
260+
261+
@Agent
262+
String invoke(@V("name") String name);
263+
}
264+
253265
}

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

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,31 @@
1515
*/
1616
package io.serverlessworkflow.fluent.agentic.langchain4j;
1717

18+
import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow;
19+
import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn;
20+
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.*;
1821
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.AudienceEditor;
1922
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.CreativeWriter;
2023
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.StyleEditor;
2124
import static io.serverlessworkflow.fluent.agentic.langchain4j.Models.BASE_MODEL;
25+
import static org.junit.jupiter.api.Assertions.assertNotNull;
26+
import static org.junit.jupiter.api.Assertions.assertTrue;
2227
import static org.mockito.ArgumentMatchers.any;
2328
import static org.mockito.ArgumentMatchers.eq;
2429
import static org.mockito.Mockito.spy;
2530
import static org.mockito.Mockito.verify;
2631

27-
import dev.langchain4j.agentic.AgenticServices;
2832
import dev.langchain4j.agentic.UntypedAgent;
33+
import dev.langchain4j.agentic.scope.AgenticScope;
2934
import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder;
35+
36+
import java.util.List;
3037
import java.util.Map;
38+
import java.util.function.Function;
39+
import java.util.function.Predicate;
40+
41+
import io.serverlessworkflow.fluent.agentic.AgenticServices;
42+
import io.serverlessworkflow.fluent.agentic.AgentsUtils;
3143
import org.junit.jupiter.api.Test;
3244

3345
public class WorkflowAgentsIT {
@@ -38,21 +50,21 @@ void sequential_agents_tests() {
3850

3951
CreativeWriter creativeWriter =
4052
spy(
41-
AgenticServices.agentBuilder(CreativeWriter.class)
53+
dev.langchain4j.agentic.AgenticServices.agentBuilder(CreativeWriter.class)
4254
.chatModel(BASE_MODEL)
4355
.outputName("story")
4456
.build());
4557

4658
AudienceEditor audienceEditor =
4759
spy(
48-
AgenticServices.agentBuilder(AudienceEditor.class)
60+
dev.langchain4j.agentic.AgenticServices.agentBuilder(AudienceEditor.class)
4961
.chatModel(BASE_MODEL)
5062
.outputName("story")
5163
.build());
5264

5365
StyleEditor styleEditor =
5466
spy(
55-
AgenticServices.agentBuilder(StyleEditor.class)
67+
dev.langchain4j.agentic.AgenticServices.agentBuilder(StyleEditor.class)
5668
.chatModel(BASE_MODEL)
5769
.outputName("story")
5870
.build());
@@ -77,4 +89,76 @@ void sequential_agents_tests() {
7789
verify(audienceEditor).editStory(any(), eq("young adults"));
7890
verify(styleEditor).editStory(any(), eq("fantasy"));
7991
}
92+
93+
@Test
94+
public void sequenceHelperTest() {
95+
var creativeWriter = AgentsUtils.newCreativeWriter();
96+
var audienceEditor = AgentsUtils.newAudienceEditor();
97+
var styleEditor = AgentsUtils.newStyleEditor();
98+
99+
NovelCreator novelCreator = AgenticServices.of(NovelCreator.class)
100+
.flow(workflow("seqFlow")
101+
.sequence(creativeWriter, audienceEditor, styleEditor)
102+
).build();
103+
104+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
105+
assertNotNull(story);
106+
}
107+
108+
@Test
109+
public void parallelWorkflow() {
110+
var foodExpert = AgentsUtils.newFoodExpert();
111+
var movieExpert = AgentsUtils.newMovieExpert();
112+
113+
EveningPlannerAgent eveningPlannerAgent = AgenticServices.of(EveningPlannerAgent.class)
114+
.flow(workflow("parallelFlow")
115+
.parallel(foodExpert, movieExpert)
116+
).build();
117+
List<EveningPlan> result = eveningPlannerAgent.plan("romantic");
118+
assertTrue(result.size() > 0);
119+
}
120+
121+
@Test
122+
public void loopTest() {
123+
var creativeWriter = AgentsUtils.newCreativeWriter();
124+
var scorer = AgentsUtils.newStyleScorer();
125+
var editor = AgentsUtils.newStyleEditor();
126+
127+
Predicate<AgenticScope> until = s -> s.readState("score", 0).doubleValue() >= 0.8;
128+
129+
130+
StyledWriter styledWriter = AgenticServices.of(StyledWriter.class)
131+
.flow(workflow("loopFlow").agent(creativeWriter)
132+
.loop(until, scorer, editor)
133+
).build();
134+
135+
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "fantasy");
136+
assertNotNull(story);
137+
}
138+
139+
@Test
140+
public void humanInTheLoop() {
141+
var astrologyAgent = AgentsUtils.newAstrologyAgent();
142+
143+
var askSign = new Function<Map<String, Object>, Map<String, Object>>() {
144+
@Override
145+
public Map<String, Object> apply(Map<String, Object> holder) {
146+
System.out.println("What's your star sign?");
147+
//var sign = System.console().readLine();
148+
holder.put("sign", "piscis");
149+
return holder;
150+
}
151+
};
152+
153+
String result = AgenticServices.of(HoroscopeAgent.class)
154+
.flow(workflow("humanInTheLoop")
155+
.tasks(tasks -> tasks.callFn(fn(askSign)))
156+
.agent(astrologyAgent))
157+
.build()
158+
.invoke("My name is Mario. What is my horoscope?");
159+
160+
assertNotNull(result);
161+
162+
}
163+
80164
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.service.V;
20+
import io.serverlessworkflow.api.types.Workflow;
21+
import io.serverlessworkflow.impl.WorkflowApplication;
22+
23+
import java.lang.annotation.Annotation;
24+
import java.lang.reflect.InvocationHandler;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Proxy;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
import java.util.Objects;
30+
31+
public class AgenticServices<T> {
32+
33+
private final Class<T> agent;
34+
35+
private AgentWorkflowBuilder builder;
36+
37+
private AgenticServices(Class<T> agent) {
38+
this.agent = agent;
39+
}
40+
41+
public static <T> AgenticServices<T> of(Class<T> agent) {
42+
return new AgenticServices<>(agent);
43+
}
44+
45+
public AgenticServices<T> flow(AgentWorkflowBuilder builder) {
46+
this.builder = builder;
47+
return this;
48+
}
49+
50+
public T build() {
51+
Objects.requireNonNull(builder, "AgenticServices.flow(AgentWorkflowBuilder) must be called before build()");
52+
Workflow workflow = builder.build();
53+
return AgenticServiceBuilder.create(agent, new AgentInvocationHandler(workflow));
54+
}
55+
56+
57+
private static class AgenticServiceBuilder {
58+
59+
@SuppressWarnings("unchecked")
60+
public static <T> T create(Class<T> runner, InvocationHandler h) {
61+
if (!runner.isInterface()) {
62+
throw new IllegalArgumentException(runner + " must be an interface to create a Proxy");
63+
}
64+
65+
ClassLoader cl = runner.getClassLoader();
66+
Class<?>[] ifaces = new Class<?>[]{runner};
67+
return (T) Proxy.newProxyInstance(cl, ifaces, h);
68+
}
69+
}
70+
71+
private static class AgentInvocationHandler implements InvocationHandler {
72+
73+
private final Workflow workflow;
74+
75+
public AgentInvocationHandler(Workflow workflow) {
76+
this.workflow = workflow;
77+
}
78+
79+
@Override
80+
public Object invoke(Object proxy, Method method, Object[] args) {
81+
if (method.getDeclaringClass() == Object.class) {
82+
return switch (method.getName()) {
83+
case "toString" -> "AgentProxy(" + workflow.getDocument().getName() + ")";
84+
case "hashCode" -> System.identityHashCode(proxy);
85+
case "equals" -> proxy == args[0];
86+
default -> throw new IllegalStateException("Unexpected Object method: " + method);
87+
};
88+
}
89+
90+
Agent agent = method.getAnnotation(Agent.class);
91+
if (agent == null) {
92+
throw new IllegalStateException("Method " + method.getName() + " is not annotated with @Agent");
93+
}
94+
95+
Annotation[][] annotations = method.getParameterAnnotations();
96+
Map<String, Object> input = new HashMap<>();
97+
for (int i = 0; i < annotations.length; i++) {
98+
boolean found = false;
99+
for (Annotation a : annotations[i]) {
100+
if (a instanceof V) {
101+
String key = ((V) a).value();
102+
Object value = args[i];
103+
input.put(key, value);
104+
found = true;
105+
break;
106+
}
107+
}
108+
if (!found) {
109+
throw new IllegalStateException("Parameter " + (i + 1) + " of method " + method.getName() + " is not annotated with @V");
110+
}
111+
}
112+
113+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
114+
return app.workflowDefinition(workflow).instance(input).start().get().asJavaObject();
115+
} catch (Exception e) {
116+
throw new RuntimeException("Workflow execution failed", e);
117+
}
118+
}
119+
}
120+
121+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,12 @@ public void sequentialWorkflow() {
7777
@Test
7878
@DisplayName("Looping agents via DSL.loop(...)")
7979
public void loopWorkflow() {
80-
80+
var creativeWriter = AgentsUtils.newCreativeWriter();
8181
var scorer = AgentsUtils.newStyleScorer();
8282
var editor = AgentsUtils.newStyleEditor();
8383

8484
Workflow wf =
85-
AgentWorkflowBuilder.workflow("retryFlow")
85+
AgentWorkflowBuilder.workflow("retryFlow").agent(creativeWriter)
8686
.loop("reviewLoop", c -> c.readState("score", 0).doubleValue() >= 0.8, scorer, editor)
8787
.build();
8888

0 commit comments

Comments
 (0)