Skip to content

Commit 0089734

Browse files
committed
Added AgenticServices helper
Signed-off-by: Dmitrii Tikhomirov <[email protected]>
1 parent fee8305 commit 0089734

File tree

8 files changed

+333
-14
lines changed

8 files changed

+333
-14
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,9 @@ public <T> Optional<T> as(Class<T> clazz) {
5555
return super.as(clazz);
5656
}
5757
}
58+
59+
@Override
60+
public Object asJavaObject() {
61+
return agenticScope;
62+
}
5863
}

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
import io.serverlessworkflow.impl.WorkflowModelFactory;
2424
import io.serverlessworkflow.impl.expressions.agentic.langchain4j.AgenticScopeRegistryAssessor;
2525
import java.time.OffsetDateTime;
26+
import java.util.HashMap;
2627
import java.util.Map;
27-
import java.util.stream.Collectors;
2828

2929
class AgenticModelFactory implements WorkflowModelFactory {
3030

@@ -55,17 +55,22 @@ 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
}
6162

6263
@Override
6364
public WorkflowModel combine(Map<String, WorkflowModel> workflowVariables) {
64-
Map<String, Object> combinedState =
65-
workflowVariables.entrySet().stream()
66-
.map(e -> Map.entry(e.getKey(), e.getValue().asJavaObject()))
67-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
68-
return newAgenticModel(combinedState);
65+
Map<String, Object> map = new HashMap<>();
66+
for (Map.Entry<String, WorkflowModel> e : workflowVariables.entrySet()) {
67+
if (e.getValue() instanceof AgenticModel agenticModel) {
68+
map.putAll(agenticModel.getAgenticScope().state());
69+
} else {
70+
map.put(e.getKey(), e.getValue().asJavaObject());
71+
}
72+
}
73+
return newAgenticModel(map);
6974
}
7075

7176
@Override

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

Lines changed: 7 additions & 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 {
@@ -250,4 +248,10 @@ public interface EveningPlannerAgent {
250248
@Agent
251249
List<EveningPlan> plan(@V("mood") String mood);
252250
}
251+
252+
public interface HoroscopeAgent {
253+
254+
@Agent
255+
String invoke(@V("name") String name);
256+
}
253257
}

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

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,37 @@
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.AgentsUtils.newAstrologyAgent;
20+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAudienceEditor;
21+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newCreativeWriter;
22+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newFoodExpert;
23+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newMovieExpert;
24+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleEditor;
25+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleScorer;
26+
import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newSummaryStory;
27+
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.*;
1828
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.AudienceEditor;
1929
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.CreativeWriter;
2030
import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.StyleEditor;
2131
import static io.serverlessworkflow.fluent.agentic.langchain4j.Models.BASE_MODEL;
32+
import static org.junit.jupiter.api.Assertions.assertEquals;
33+
import static org.junit.jupiter.api.Assertions.assertNotNull;
2234
import static org.mockito.ArgumentMatchers.any;
2335
import static org.mockito.ArgumentMatchers.eq;
2436
import static org.mockito.Mockito.spy;
2537
import static org.mockito.Mockito.verify;
2638

27-
import dev.langchain4j.agentic.AgenticServices;
2839
import dev.langchain4j.agentic.UntypedAgent;
40+
import dev.langchain4j.agentic.scope.AgenticScope;
2941
import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder;
42+
import io.serverlessworkflow.fluent.agentic.AgenticServices;
43+
import io.serverlessworkflow.fluent.agentic.AgentsUtils;
44+
import java.util.List;
3045
import java.util.Map;
46+
import java.util.function.Function;
47+
import java.util.function.Predicate;
48+
import java.util.stream.IntStream;
3149
import org.junit.jupiter.api.Test;
3250

3351
public class WorkflowAgentsIT {
@@ -38,21 +56,21 @@ void sequential_agents_tests() {
3856

3957
CreativeWriter creativeWriter =
4058
spy(
41-
AgenticServices.agentBuilder(CreativeWriter.class)
59+
dev.langchain4j.agentic.AgenticServices.agentBuilder(CreativeWriter.class)
4260
.chatModel(BASE_MODEL)
4361
.outputName("story")
4462
.build());
4563

4664
AudienceEditor audienceEditor =
4765
spy(
48-
AgenticServices.agentBuilder(AudienceEditor.class)
66+
dev.langchain4j.agentic.AgenticServices.agentBuilder(AudienceEditor.class)
4967
.chatModel(BASE_MODEL)
5068
.outputName("story")
5169
.build());
5270

5371
StyleEditor styleEditor =
5472
spy(
55-
AgenticServices.agentBuilder(StyleEditor.class)
73+
dev.langchain4j.agentic.AgenticServices.agentBuilder(StyleEditor.class)
5674
.chatModel(BASE_MODEL)
5775
.outputName("story")
5876
.build());
@@ -77,4 +95,123 @@ void sequential_agents_tests() {
7795
verify(audienceEditor).editStory(any(), eq("young adults"));
7896
verify(styleEditor).editStory(any(), eq("fantasy"));
7997
}
98+
99+
@Test
100+
public void sequenceHelperTest() {
101+
var creativeWriter = newCreativeWriter();
102+
var audienceEditor = newAudienceEditor();
103+
var styleEditor = newStyleEditor();
104+
105+
AgentsUtils.NovelCreator novelCreator =
106+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(AgentsUtils.NovelCreator.class)
107+
.flow(workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor))
108+
.build();
109+
110+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
111+
assertNotNull(story);
112+
}
113+
114+
@Test
115+
public void agentAndSequenceHelperTest() {
116+
var creativeWriter = newCreativeWriter();
117+
var audienceEditor = newAudienceEditor();
118+
var styleEditor = newStyleEditor();
119+
120+
AgentsUtils.NovelCreator novelCreator =
121+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(AgentsUtils.NovelCreator.class)
122+
.flow(workflow("seqFlow").agent(creativeWriter).sequence(audienceEditor, styleEditor))
123+
.build();
124+
125+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
126+
assertNotNull(story);
127+
}
128+
129+
@Test
130+
public void agentAndSequenceAndAgentHelperTest() {
131+
var creativeWriter = newCreativeWriter();
132+
var audienceEditor = newAudienceEditor();
133+
var styleEditor = newStyleEditor();
134+
var summaryStory = newSummaryStory();
135+
136+
AgentsUtils.NovelCreator novelCreator =
137+
io.serverlessworkflow.fluent.agentic.AgenticServices.of(AgentsUtils.NovelCreator.class)
138+
.flow(
139+
workflow("seqFlow")
140+
.agent(creativeWriter)
141+
.sequence(audienceEditor, styleEditor)
142+
.agent(summaryStory))
143+
.build();
144+
145+
String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
146+
assertNotNull(story);
147+
}
148+
149+
@Test
150+
public void parallelWorkflow() {
151+
var foodExpert = newFoodExpert();
152+
var movieExpert = newMovieExpert();
153+
154+
Function<AgenticScope, List<EveningPlan>> planEvening =
155+
input -> {
156+
List<String> movies = (List<String>) input.readState("movies");
157+
List<String> meals = (List<String>) input.readState("meals");
158+
159+
int max = Math.min(movies.size(), meals.size());
160+
return IntStream.range(0, max)
161+
.mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i)))
162+
.toList();
163+
};
164+
165+
EveningPlannerAgent eveningPlannerAgent =
166+
AgenticServices.of(EveningPlannerAgent.class)
167+
.flow(workflow("parallelFlow").parallel(foodExpert, movieExpert).outputAs(planEvening))
168+
.build();
169+
List<EveningPlan> result = eveningPlannerAgent.plan("romantic");
170+
assertEquals(3, result.size());
171+
}
172+
173+
@Test
174+
public void loopTest() {
175+
var creativeWriter = newCreativeWriter();
176+
var scorer = newStyleScorer();
177+
var editor = newStyleEditor();
178+
179+
Predicate<AgenticScope> until = s -> s.readState("score", 0.0) >= 0.8;
180+
181+
StyledWriter styledWriter =
182+
AgenticServices.of(StyledWriter.class)
183+
.flow(workflow("loopFlow").agent(creativeWriter).loop(until, scorer, editor))
184+
.build();
185+
186+
String story = styledWriter.writeStoryWithStyle("dragons and wizards", "fantasy");
187+
assertNotNull(story);
188+
}
189+
190+
@Test
191+
public void humanInTheLoop() {
192+
var astrologyAgent = newAstrologyAgent();
193+
194+
var askSign =
195+
new Function<AgenticScope, AgenticScope>() {
196+
@Override
197+
public AgenticScope apply(AgenticScope holder) {
198+
System.out.println("What's your star sign?");
199+
// var sign = System.console().readLine();
200+
holder.writeState("sign", "piscis");
201+
return holder;
202+
}
203+
};
204+
205+
String result =
206+
AgenticServices.of(Agents.HoroscopeAgent.class)
207+
.flow(
208+
workflow("humanInTheLoop")
209+
.inputFrom(askSign)
210+
// .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too
211+
.agent(astrologyAgent))
212+
.build()
213+
.invoke("My name is Mario. What is my horoscope?");
214+
215+
assertNotNull(result);
216+
}
80217
}
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+
}

0 commit comments

Comments
 (0)