Skip to content

Commit ee7b32b

Browse files
committed
Agentic workflow examples
Signed-off-by: Dmitrii Tikhomirov <[email protected]>
1 parent 640c484 commit ee7b32b

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import dev.langchain4j.agentic.Agent;
1919
import dev.langchain4j.agentic.internal.AgentSpecification;
20+
import dev.langchain4j.service.SystemMessage;
2021
import dev.langchain4j.service.UserMessage;
2122
import dev.langchain4j.service.V;
2223
import java.util.List;
@@ -234,4 +235,87 @@ String draftNew(
234235
@V("allowedDomains") List<String> allowedDomains,
235236
@V("links") List<String> links);
236237
}
238+
239+
interface CreativeWriter {
240+
241+
@UserMessage(
242+
"""
243+
You are a creative writer.
244+
Generate a draft of a story no more than
245+
3 sentences long around the given topic.
246+
Return only the story and nothing else.
247+
The topic is {{topic}}.
248+
""")
249+
@Agent("Generates a story based on the given topic")
250+
String generateStory(@V("topic") String topic);
251+
}
252+
253+
interface AudienceEditor {
254+
255+
@UserMessage(
256+
"""
257+
You are a professional editor.
258+
Analyze and rewrite the following story to better align
259+
with the target audience of {{audience}}.
260+
Return only the story and nothing else.
261+
The story is "{{story}}".
262+
""")
263+
@Agent("Edits a story to better fit a given audience")
264+
String editStory(@V("story") String story, @V("audience") String audience);
265+
}
266+
267+
interface StyleEditor {
268+
269+
@UserMessage(
270+
"""
271+
You are a professional editor.
272+
Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style.
273+
Return only the story and nothing else.
274+
The story is "{{story}}".
275+
""")
276+
@Agent("Edits a story to better fit a given style")
277+
String editStory(@V("story") String story, @V("style") String style);
278+
}
279+
280+
interface StyleScorer {
281+
282+
@UserMessage(
283+
"""
284+
You are a critical reviewer.
285+
Give a review score between 0.0 and 1.0 for the following
286+
story based on how well it aligns with the style '{{style}}'.
287+
Return only the score and nothing else.
288+
289+
The story is: "{{story}}"
290+
""")
291+
@Agent("Scores a story based on how well it aligns with a given style")
292+
double scoreStyle(@V("story") String story, @V("style") String style);
293+
}
294+
295+
interface FoodExpert {
296+
297+
@UserMessage(
298+
"""
299+
You are a great evening planner.
300+
Propose a list of 3 meals matching the given mood.
301+
The mood is {{mood}}.
302+
For each meal, just give the name of the meal.
303+
Provide a list with the 3 items and nothing else.
304+
""")
305+
@Agent
306+
List<String> findMeal(@V("mood") String mood);
307+
}
308+
309+
interface AstrologyAgent {
310+
@SystemMessage(
311+
"""
312+
You are an astrologist that generates horoscopes based on the user's name and zodiac sign.
313+
""")
314+
@UserMessage(
315+
"""
316+
Generate the horoscope for {{name}} who is a {{sign}}.
317+
""")
318+
@Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.")
319+
String horoscope(@V("name") String name, @V("sign") String sign);
320+
}
237321
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,52 @@ public static Agents.MovieExpert newMovieExpert() {
3131
.chatModel(BASE_MODEL)
3232
.build());
3333
}
34+
35+
public static Agents.CreativeWriter newCreativeWriter() {
36+
return spy(
37+
AgenticServices.agentBuilder(Agents.CreativeWriter.class)
38+
.outputName("story")
39+
.chatModel(BASE_MODEL)
40+
.build());
41+
}
42+
43+
public static Agents.AudienceEditor newAudienceEditor() {
44+
return spy(
45+
AgenticServices.agentBuilder(Agents.AudienceEditor.class)
46+
.outputName("story")
47+
.chatModel(BASE_MODEL)
48+
.build());
49+
}
50+
51+
public static Agents.StyleEditor newStyleEditor() {
52+
return spy(
53+
AgenticServices.agentBuilder(Agents.StyleEditor.class)
54+
.outputName("story")
55+
.chatModel(BASE_MODEL)
56+
.build());
57+
}
58+
59+
public static Agents.StyleScorer newStyleScorer() {
60+
return spy(
61+
AgenticServices.agentBuilder(Agents.StyleScorer.class)
62+
.outputName("score")
63+
.chatModel(BASE_MODEL)
64+
.build());
65+
}
66+
67+
public static Agents.FoodExpert newFoodExpert() {
68+
return spy(
69+
AgenticServices.agentBuilder(Agents.FoodExpert.class)
70+
.chatModel(BASE_MODEL)
71+
.outputName("meals")
72+
.build());
73+
}
74+
75+
public static Agents.AstrologyAgent newAstrologyAgent() {
76+
return spy(
77+
AgenticServices.agentBuilder(Agents.AstrologyAgent.class)
78+
.chatModel(BASE_MODEL)
79+
.outputName("horoscope")
80+
.build());
81+
}
3482
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 org.assertj.core.api.Assertions.assertThat;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
22+
import dev.langchain4j.agentic.AgenticServices;
23+
import dev.langchain4j.agentic.workflow.HumanInTheLoop;
24+
import io.serverlessworkflow.api.types.TaskItem;
25+
import io.serverlessworkflow.api.types.Workflow;
26+
import io.serverlessworkflow.api.types.func.CallTaskJava;
27+
import io.serverlessworkflow.api.types.func.ForTaskFunction;
28+
import io.serverlessworkflow.impl.WorkflowApplication;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.concurrent.atomic.AtomicReference;
32+
import org.junit.jupiter.api.DisplayName;
33+
import org.junit.jupiter.api.Test;
34+
35+
public class LC4JEquivalenceIT {
36+
37+
@Test
38+
@DisplayName("Sequential agents via DSL.sequence(...)")
39+
public void sequentialWorkflow() {
40+
var a1 = AgentsUtils.newCreativeWriter();
41+
var a2 = AgentsUtils.newAudienceEditor();
42+
var a3 = AgentsUtils.newStyleEditor();
43+
44+
Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build();
45+
46+
List<TaskItem> items = wf.getDo();
47+
assertThat(items).hasSize(3);
48+
49+
assertThat(items.get(0).getName()).isEqualTo("process-0");
50+
assertThat(items.get(1).getName()).isEqualTo("process-1");
51+
assertThat(items.get(2).getName()).isEqualTo("process-2");
52+
items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
53+
54+
Map<String, Object> input =
55+
Map.of(
56+
"topic", "dragons and wizards",
57+
"style", "fantasy",
58+
"audience", "young adults");
59+
60+
Map<String, Object> result;
61+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
62+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
63+
} catch (Exception e) {
64+
throw new RuntimeException("Workflow execution failed", e);
65+
}
66+
67+
assertThat(result).containsKey("story");
68+
}
69+
70+
@Test
71+
@DisplayName("Looping agents via DSL.loop(...)") // TODO maxIterations(5)
72+
public void loopWorkflow() {
73+
74+
var scorer = AgentsUtils.newStyleScorer();
75+
var editor = AgentsUtils.newStyleEditor();
76+
77+
Workflow wf =
78+
AgentWorkflowBuilder.workflow("retryFlow")
79+
.loop("reviewLoop", c -> c.readState("score", 0).doubleValue() >= 0.8, scorer, editor)
80+
.build();
81+
82+
List<TaskItem> items = wf.getDo();
83+
assertThat(items).hasSize(1);
84+
85+
var fn = (ForTaskFunction) items.get(0).getTask().getForTask();
86+
assertThat(fn.getDo()).isNotNull();
87+
assertThat(fn.getDo()).hasSize(2);
88+
fn.getDo()
89+
.forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class));
90+
91+
Map<String, Object> input =
92+
Map.of(
93+
"story", "dragons and wizards",
94+
"style", "comedy");
95+
96+
Map<String, Object> result;
97+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
98+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
99+
} catch (Exception e) {
100+
throw new RuntimeException("Workflow execution failed", e);
101+
}
102+
103+
assertThat(result).containsKey("story");
104+
}
105+
106+
@Test
107+
@DisplayName("Parallel agents via DSL.parallel(...)")
108+
public void parallelWorkflow() {
109+
var a1 = AgentsUtils.newFoodExpert();
110+
var a2 = AgentsUtils.newMovieExpert();
111+
112+
Workflow wf = workflow("forkFlow").parallel("fanout", a1, a2).build();
113+
114+
List<TaskItem> items = wf.getDo();
115+
assertThat(items).hasSize(1);
116+
117+
var fork = items.get(0).getTask().getForkTask();
118+
assertThat(fork.getFork().getBranches()).hasSize(2);
119+
assertThat(fork.getFork().getBranches().get(0).getName()).isEqualTo("branch-0-fanout");
120+
assertThat(fork.getFork().getBranches().get(1).getName()).isEqualTo("branch-1-fanout");
121+
122+
Map<String, Object> input = Map.of("mood", "I am hungry and bored");
123+
124+
Map<String, Object> result;
125+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
126+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
127+
} catch (Exception e) {
128+
throw new RuntimeException("Workflow execution failed", e);
129+
}
130+
131+
assertEquals("Fake conflict response", result.get("meals"));
132+
assertEquals("Fake conflict response", result.get("movies"));
133+
}
134+
135+
@Test
136+
@DisplayName("Human in the loop")
137+
public void humanInTheLoop() {
138+
139+
AtomicReference<String> request = new AtomicReference<>();
140+
141+
HumanInTheLoop humanInTheLoop =
142+
AgenticServices.humanInTheLoopBuilder()
143+
.description("Please provide the horoscope request")
144+
.inputName("request")
145+
.outputName("sign")
146+
.requestWriter(q -> request.set("My name is Mario. What is my horoscope?"))
147+
.responseReader(() -> "piscis")
148+
.build();
149+
150+
var a1 = AgentsUtils.newAstrologyAgent();
151+
152+
Workflow wf =
153+
workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, humanInTheLoop)).build();
154+
155+
assertThat(wf.getDo()).hasSize(2);
156+
157+
Map<String, Object> input = Map.of("request", "My name is Mario. What is my horoscope?");
158+
159+
Map<String, Object> result;
160+
try (WorkflowApplication app = WorkflowApplication.builder().build()) {
161+
result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
162+
} catch (Exception e) {
163+
throw new RuntimeException("Workflow execution failed", e);
164+
}
165+
166+
assertThat(request.get()).isEqualTo("My name is Mario. What is my horoscope?");
167+
assertThat(result).containsEntry("sign", "piscis");
168+
}
169+
}

0 commit comments

Comments
 (0)