Skip to content

Commit 9116654

Browse files
authored
Added AgenticServices helper (#835)
* Added AgenticServices helper Signed-off-by: Dmitrii Tikhomirov <chani.liet@gmail.com> * AgenticServices rename to AgenticWorkflow Signed-off-by: Dmitrii Tikhomirov <chani.liet@gmail.com> --------- Signed-off-by: Dmitrii Tikhomirov <chani.liet@gmail.com>
1 parent 5dd9291 commit 9116654

File tree

8 files changed

+330
-10
lines changed

8 files changed

+330
-10
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: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,38 @@
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

2739
import dev.langchain4j.agentic.AgenticServices;
2840
import dev.langchain4j.agentic.UntypedAgent;
41+
import dev.langchain4j.agentic.scope.AgenticScope;
2942
import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder;
43+
import io.serverlessworkflow.fluent.agentic.AgenticWorkflow;
44+
import io.serverlessworkflow.fluent.agentic.AgentsUtils;
45+
import java.util.List;
3046
import java.util.Map;
47+
import java.util.function.Function;
48+
import java.util.function.Predicate;
49+
import java.util.stream.IntStream;
3150
import org.junit.jupiter.api.Test;
3251

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