Skip to content

Commit f86a421

Browse files
committed
we can do better
Signed-off-by: Dmitrii Tikhomirov <chani.liet@gmail.com>
1 parent c127963 commit f86a421

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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 static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow;
19+
import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional;
20+
import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks;
21+
import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn;
22+
import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.loop;
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
26+
import dev.langchain4j.agentic.AgenticServices;
27+
import dev.langchain4j.agentic.scope.AgenticScope;
28+
import dev.langchain4j.agentic.workflow.HumanInTheLoop;
29+
import io.serverlessworkflow.api.types.TaskItem;
30+
import io.serverlessworkflow.api.types.Workflow;
31+
import io.serverlessworkflow.api.types.func.CallTaskJava;
32+
import io.serverlessworkflow.api.types.func.ForTaskFunction;
33+
import io.serverlessworkflow.impl.WorkflowApplication;
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.concurrent.atomic.AtomicReference;
38+
import java.util.function.Predicate;
39+
import org.junit.jupiter.api.DisplayName;
40+
import org.junit.jupiter.api.Test;
41+
42+
public class LC4JEquivalenceIT {
43+
44+
@Test
45+
@DisplayName("Sequential agents via DSL.sequence(...)")
46+
public void sequentialWorkflow() {
47+
var creativeWriter = AgentsUtils.newCreativeWriter();
48+
var audienceEditor = AgentsUtils.newAudienceEditor();
49+
var styleEditor = AgentsUtils.newStyleEditor();
50+
51+
Workflow wf = workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor).build();
52+
53+
List<TaskItem> items = wf.getDo();
54+
assertThat(items).hasSize(3);
55+
56+
assertThat(items.get(0).getName()).isEqualTo("process-0");
57+
assertThat(items.get(1).getName()).isEqualTo("process-1");
58+
assertThat(items.get(2).getName()).isEqualTo("process-2");
59+
items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
60+
61+
Map<String, Object> input =
62+
Map.of(
63+
"topic", "dragons and wizards",
64+
"style", "fantasy",
65+
"audience", "young adults");
66+
67+
Map<String, Object> result;
68+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
69+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
70+
} catch (Exception e) {
71+
throw new RuntimeException("Workflow execution failed", e);
72+
}
73+
74+
assertThat(result).containsKey("story");
75+
}
76+
77+
@Test
78+
@DisplayName("Looping agents via DSL.loop(...)")
79+
public void loopWorkflow() {
80+
var creativeWriter = AgentsUtils.newCreativeWriter();
81+
var styleScorer = AgentsUtils.newStyleScorer();
82+
var styleEditor = AgentsUtils.newStyleEditor();
83+
84+
Workflow wf =
85+
AgentWorkflowBuilder.workflow("retryFlow")
86+
.agent(creativeWriter)
87+
.loop(
88+
"reviewLoop",
89+
c -> c.readState("score", 0).doubleValue() >= 0.8,
90+
styleScorer,
91+
styleEditor)
92+
.build();
93+
94+
List<TaskItem> items = wf.getDo();
95+
assertThat(items).hasSize(1);
96+
97+
var fn = (ForTaskFunction) items.get(0).getTask().getForTask();
98+
assertThat(fn.getDo()).isNotNull();
99+
assertThat(fn.getDo()).hasSize(2);
100+
fn.getDo()
101+
.forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
102+
103+
Map<String, Object> input =
104+
Map.of(
105+
"story", "dragons and wizards",
106+
"style", "comedy");
107+
108+
Map<String, Object> result;
109+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
110+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
111+
} catch (Exception e) {
112+
throw new RuntimeException("Workflow execution failed", e);
113+
}
114+
115+
assertThat(result).containsKey("story");
116+
}
117+
118+
@Test
119+
@DisplayName("Looping agents via DSL.loop(...)")
120+
public void loopWorkflowWithMaxIterations() {
121+
var scorer = AgentsUtils.newStyleScorer();
122+
var editor = AgentsUtils.newStyleEditor();
123+
124+
Predicate<AgenticScope> until = s -> s.readState("score", 0).doubleValue() >= 0.8;
125+
126+
Workflow wf = workflow("retryFlow").tasks(loop(5, until, scorer, editor)).build();
127+
128+
List<TaskItem> items = wf.getDo();
129+
assertThat(items).hasSize(1);
130+
131+
var fn = (ForTaskFunction) items.get(0).getTask().getForTask();
132+
assertThat(fn.getDo()).isNotNull();
133+
assertThat(fn.getDo()).hasSize(2);
134+
fn.getDo()
135+
.forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
136+
137+
Map<String, Object> input =
138+
Map.of(
139+
"story", "dragons and wizards",
140+
"style", "comedy");
141+
142+
Map<String, Object> result;
143+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
144+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
145+
} catch (Exception e) {
146+
throw new RuntimeException("Workflow execution failed", e);
147+
}
148+
149+
assertThat(result).containsKey("story");
150+
}
151+
152+
public record EveningPlan(String movie, String meal) {}
153+
154+
@Test
155+
@DisplayName("Parallel agents via DSL.parallel(...)")
156+
public void parallelWorkflow() {
157+
var foodExpert = AgentsUtils.newFoodExpert();
158+
var movieExpert = AgentsUtils.newMovieExpert();
159+
160+
workflow("forkFlow")
161+
.tasks(
162+
d ->
163+
d.parallel(foodExpert, movieExpert)
164+
.callFn(
165+
fn(
166+
f -> {
167+
Map<String, List<String>> asMap = (Map<String, List<String>>) f;
168+
List<EveningPlan> result = new ArrayList<>();
169+
int max =
170+
asMap.values().stream()
171+
.map(List::size)
172+
.min(Integer::compareTo)
173+
.orElse(0);
174+
for (int i = 0; i < max; i++) {
175+
result.add(
176+
new EveningPlan(
177+
asMap.get("movies").get(i), asMap.get("meals").get(i)));
178+
}
179+
return result;
180+
})))
181+
.build();
182+
183+
Workflow wf = workflow("forkFlow")
184+
.tasks(d -> d
185+
.parallel("fanout", foodExpert, movieExpert)
186+
.callFn(fn((Map<String, List<String>> m) -> {
187+
var movies = m.getOrDefault("movies", List.of());
188+
var meals = m.getOrDefault("meals", List.of());
189+
return java.util.stream.IntStream
190+
.range(0, Math.min(movies.size(), meals.size()))
191+
.mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i)))
192+
.toList();
193+
}))
194+
).build();
195+
196+
List<TaskItem> items = wf.getDo();
197+
assertThat(items).hasSize(1);
198+
199+
var fork = items.get(0).getTask().getForkTask();
200+
// two branches created
201+
assertThat(fork.getFork().getBranches()).hasSize(2);
202+
// branch names follow "branch-{index}-{name}"
203+
assertThat(fork.getFork().getBranches().get(0).getName()).isEqualTo("branch-0-fanout");
204+
assertThat(fork.getFork().getBranches().get(1).getName()).isEqualTo("branch-1-fanout");
205+
206+
Map<String, Object> input = Map.of("mood", "I am hungry and bored");
207+
208+
Map<String, Object> result;
209+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
210+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
211+
} catch (Exception e) {
212+
throw new RuntimeException("Workflow execution failed", e);
213+
}
214+
215+
assertEquals("Fake conflict response", result.get("meals"));
216+
assertEquals("Fake conflict response", result.get("movies"));
217+
}
218+
219+
@Test
220+
@DisplayName("Error handling with agents")
221+
public void errorHandling() {
222+
var creativeWriter = AgentsUtils.newCreativeWriter();
223+
var audienceEditor = AgentsUtils.newAudienceEditor();
224+
var styleEditor = AgentsUtils.newStyleEditor();
225+
226+
Workflow wf =
227+
workflow("seqFlow")
228+
.sequence("process", creativeWriter, audienceEditor, styleEditor)
229+
.build();
230+
231+
List<TaskItem> items = wf.getDo();
232+
assertThat(items).hasSize(3);
233+
234+
assertThat(items.get(0).getName()).isEqualTo("process-0");
235+
assertThat(items.get(1).getName()).isEqualTo("process-1");
236+
assertThat(items.get(2).getName()).isEqualTo("process-2");
237+
items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
238+
239+
Map<String, Object> input =
240+
Map.of(
241+
"style", "fantasy",
242+
"audience", "young adults");
243+
244+
Map<String, Object> result;
245+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
246+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
247+
} catch (Exception e) {
248+
throw new RuntimeException("Workflow execution failed", e);
249+
}
250+
251+
assertThat(result).containsKey("story");
252+
}
253+
254+
@SuppressWarnings("unchecked")
255+
@Test
256+
@DisplayName("Conditional agents via choice(...)")
257+
public void conditionalWorkflow() {
258+
259+
var category = AgentsUtils.newCategoryRouter();
260+
var medicalExpert = AgentsUtils.newMedicalExpert();
261+
var technicalExpert = AgentsUtils.newTechnicalExpert();
262+
var legalExpert = AgentsUtils.newLegalExpert();
263+
264+
Workflow wf =
265+
workflow("conditional")
266+
.sequence("process", category)
267+
.tasks(
268+
doTasks(
269+
conditional(Agents.RequestCategory.MEDICAL::equals, medicalExpert),
270+
conditional(Agents.RequestCategory.TECHNICAL::equals, technicalExpert),
271+
conditional(Agents.RequestCategory.LEGAL::equals, legalExpert)))
272+
.build();
273+
274+
Map<String, Object> input = Map.of("question", "What is the best treatment for a common cold?");
275+
276+
Map<String, Object> result;
277+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
278+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
279+
} catch (Exception e) {
280+
throw new RuntimeException("Workflow execution failed", e);
281+
}
282+
283+
assertThat(result).containsKey("response");
284+
}
285+
286+
@Test
287+
@DisplayName("Human in the loop")
288+
public void humanInTheLoop() {
289+
290+
AtomicReference<String> request = new AtomicReference<>();
291+
292+
HumanInTheLoop humanInTheLoop =
293+
AgenticServices.humanInTheLoopBuilder()
294+
.description("Please provide the horoscope request")
295+
.inputName("request")
296+
.outputName("sign")
297+
.requestWriter(q -> request.set("My name is Mario. What is my horoscope?"))
298+
.responseReader(() -> "piscis")
299+
.build();
300+
301+
var astrologyAgent = AgentsUtils.newAstrologyAgent();
302+
303+
Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, humanInTheLoop).build();
304+
305+
assertThat(wf.getDo()).hasSize(2);
306+
307+
Map<String, Object> input = Map.of("request", "My name is Mario. What is my horoscope?");
308+
309+
Map<String, Object> result;
310+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
311+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
312+
} catch (Exception e) {
313+
throw new RuntimeException("Workflow execution failed", e);
314+
}
315+
316+
assertThat(request.get()).isEqualTo("My name is Mario. What is my horoscope?");
317+
assertThat(result).containsEntry("sign", "piscis");
318+
}
319+
}

0 commit comments

Comments
 (0)