From bfae5de17772026072303489b839548bbbc3b04c Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 24 Sep 2025 12:59:09 -0700 Subject: [PATCH 1/7] Add loop with maxIterations to the AgenticDSL Signed-off-by: Dmitrii Tikhomirov --- .../serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java index f6505ef0..ccf77f37 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java @@ -121,6 +121,13 @@ public static AgentTaskConfigurer loop(Predicate exitCondition, Ob return list -> list.loop(l -> l.subAgents(agents).exitCondition(exitCondition)); } + public static AgentTaskConfigurer loop( + Predicate exitCondition, int maxIterations, Object... agents) { + return list -> + list.loop( + l -> l.subAgents(agents).exitCondition(exitCondition).maxIterations(maxIterations)); + } + public static AgentTaskConfigurer parallel(Object... agents) { return list -> list.parallel(agents); } From c9ae067e9ed392291f148992c15ad4246e238b95 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 23 Sep 2025 12:52:56 -0700 Subject: [PATCH 2/7] Agentic workflow examples Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/Agents.java | 84 +++++++++ .../fluent/agentic/AgentsUtils.java | 48 +++++ .../fluent/agentic/LC4JEquivalenceIT.java | 169 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 5c10764e..3c60dcd6 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -17,6 +17,7 @@ import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; +import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; @@ -234,4 +235,87 @@ String draftNew( @V("allowedDomains") List allowedDomains, @V("links") List links); } + + interface CreativeWriter { + + @UserMessage( + """ + You are a creative writer. + Generate a draft of a story no more than + 3 sentences long around the given topic. + Return only the story and nothing else. + The topic is {{topic}}. + """) + @Agent("Generates a story based on the given topic") + String generateStory(@V("topic") String topic); + } + + interface AudienceEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better align + with the target audience of {{audience}}. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given audience") + String editStory(@V("story") String story, @V("audience") String audience); + } + + interface StyleEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); + } + + interface StyleScorer { + + @UserMessage( + """ + You are a critical reviewer. + Give a review score between 0.0 and 1.0 for the following + story based on how well it aligns with the style '{{style}}'. + Return only the score and nothing else. + + The story is: "{{story}}" + """) + @Agent("Scores a story based on how well it aligns with a given style") + double scoreStyle(@V("story") String story, @V("style") String style); + } + + interface FoodExpert { + + @UserMessage( + """ + You are a great evening planner. + Propose a list of 3 meals matching the given mood. + The mood is {{mood}}. + For each meal, just give the name of the meal. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMeal(@V("mood") String mood); + } + + interface AstrologyAgent { + @SystemMessage( + """ + You are an astrologist that generates horoscopes based on the user's name and zodiac sign. + """) + @UserMessage( + """ + Generate the horoscope for {{name}} who is a {{sign}}. + """) + @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") + String horoscope(@V("name") String name, @V("sign") String sign); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java index f44d2a22..1a64e5e6 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -31,4 +31,52 @@ public static Agents.MovieExpert newMovieExpert() { .chatModel(BASE_MODEL) .build()); } + + public static Agents.CreativeWriter newCreativeWriter() { + return spy( + AgenticServices.agentBuilder(Agents.CreativeWriter.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.AudienceEditor newAudienceEditor() { + return spy( + AgenticServices.agentBuilder(Agents.AudienceEditor.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.StyleEditor newStyleEditor() { + return spy( + AgenticServices.agentBuilder(Agents.StyleEditor.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.StyleScorer newStyleScorer() { + return spy( + AgenticServices.agentBuilder(Agents.StyleScorer.class) + .outputName("score") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.FoodExpert newFoodExpert() { + return spy( + AgenticServices.agentBuilder(Agents.FoodExpert.class) + .chatModel(BASE_MODEL) + .outputName("meals") + .build()); + } + + public static Agents.AstrologyAgent newAstrologyAgent() { + return spy( + AgenticServices.agentBuilder(Agents.AstrologyAgent.class) + .chatModel(BASE_MODEL) + .outputName("horoscope") + .build()); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java new file mode 100644 index 00000000..ef41a86c --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.workflow.HumanInTheLoop; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import io.serverlessworkflow.impl.WorkflowApplication; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LC4JEquivalenceIT { + + @Test + @DisplayName("Sequential agents via DSL.sequence(...)") + public void sequentialWorkflow() { + var a1 = AgentsUtils.newCreativeWriter(); + var a2 = AgentsUtils.newAudienceEditor(); + var a3 = AgentsUtils.newStyleEditor(); + + Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build(); + + List items = wf.getDo(); + assertThat(items).hasSize(3); + + assertThat(items.get(0).getName()).isEqualTo("process-0"); + assertThat(items.get(1).getName()).isEqualTo("process-1"); + assertThat(items.get(2).getName()).isEqualTo("process-2"); + items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); + + Map input = + Map.of( + "topic", "dragons and wizards", + "style", "fantasy", + "audience", "young adults"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("story"); + } + + @Test + @DisplayName("Looping agents via DSL.loop(...)") // TODO maxIterations(5) + public void loopWorkflow() { + + var scorer = AgentsUtils.newStyleScorer(); + var editor = AgentsUtils.newStyleEditor(); + + Workflow wf = + AgentWorkflowBuilder.workflow("retryFlow") + .loop("reviewLoop", c -> c.readState("score", 0).doubleValue() >= 0.8, scorer, editor) + .build(); + + List items = wf.getDo(); + assertThat(items).hasSize(1); + + var fn = (ForTaskFunction) items.get(0).getTask().getForTask(); + assertThat(fn.getDo()).isNotNull(); + assertThat(fn.getDo()).hasSize(2); + fn.getDo() + .forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); + + Map input = + Map.of( + "story", "dragons and wizards", + "style", "comedy"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("story"); + } + + @Test + @DisplayName("Parallel agents via DSL.parallel(...)") + public void parallelWorkflow() { + var a1 = AgentsUtils.newFoodExpert(); + var a2 = AgentsUtils.newMovieExpert(); + + Workflow wf = workflow("forkFlow").parallel("fanout", a1, a2).build(); + + List items = wf.getDo(); + assertThat(items).hasSize(1); + + var fork = items.get(0).getTask().getForkTask(); + assertThat(fork.getFork().getBranches()).hasSize(2); + assertThat(fork.getFork().getBranches().get(0).getName()).isEqualTo("branch-0-fanout"); + assertThat(fork.getFork().getBranches().get(1).getName()).isEqualTo("branch-1-fanout"); + + Map input = Map.of("mood", "I am hungry and bored"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertEquals("Fake conflict response", result.get("meals")); + assertEquals("Fake conflict response", result.get("movies")); + } + + @Test + @DisplayName("Human in the loop") + public void humanInTheLoop() { + + AtomicReference request = new AtomicReference<>(); + + HumanInTheLoop humanInTheLoop = + AgenticServices.humanInTheLoopBuilder() + .description("Please provide the horoscope request") + .inputName("request") + .outputName("sign") + .requestWriter(q -> request.set("My name is Mario. What is my horoscope?")) + .responseReader(() -> "piscis") + .build(); + + var a1 = AgentsUtils.newAstrologyAgent(); + + Workflow wf = + workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, humanInTheLoop)).build(); + + assertThat(wf.getDo()).hasSize(2); + + Map input = Map.of("request", "My name is Mario. What is my horoscope?"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(request.get()).isEqualTo("My name is Mario. What is my horoscope?"); + assertThat(result).containsEntry("sign", "piscis"); + } +} From 32d65d8f4a714a644ed59b4be87991ca64dc3c00 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 23 Sep 2025 15:42:26 -0700 Subject: [PATCH 3/7] more examples Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/Agents.java | 57 ++++++++++++ .../fluent/agentic/AgentsUtils.java | 32 +++++++ .../fluent/agentic/LC4JEquivalenceIT.java | 93 ++++++++++++++++++- 3 files changed, 179 insertions(+), 3 deletions(-) diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 3c60dcd6..434867b7 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.fluent.agentic; +import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; import dev.langchain4j.service.SystemMessage; @@ -318,4 +319,60 @@ interface AstrologyAgent { @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") String horoscope(@V("name") String name, @V("sign") String sign); } + + enum RequestCategory { + LEGAL, + MEDICAL, + TECHNICAL, + UNKNOWN + } + + interface CategoryRouter { + + @UserMessage( + """ + Analyze the following user request and categorize it as 'legal', 'medical' or 'technical'. + In case the request doesn't belong to any of those categories categorize it as 'unknown'. + Reply with only one of those words and nothing else. + The user request is: '{{request}}'. + """) + @Agent("Categorizes a user request") + RequestCategory classify(@V("request") String request); + } + + interface MedicalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a medical expert. + Analyze the following user request under a medical point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A medical expert") + String medicalRequest(String request); + } + + interface LegalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a legal expert. + Analyze the following user request under a legal point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A legal expert") + String legalRequest(String request); + } + + interface TechnicalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a technical expert. + Analyze the following user request under a technical point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A technical expert") + String technicalRequest(String request); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java index 1a64e5e6..42df6b4c 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -79,4 +79,36 @@ public static Agents.AstrologyAgent newAstrologyAgent() { .outputName("horoscope") .build()); } + + public static Agents.CategoryRouter newCategoryRouter() { + return spy( + AgenticServices.agentBuilder(Agents.CategoryRouter.class) + .chatModel(BASE_MODEL) + .outputName("category") + .build()); + } + + public static Agents.MedicalExpert newMedicalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.MedicalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } + + public static Agents.TechnicalExpert newTechnicalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.TechnicalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } + + public static Agents.LegalExpert newLegalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.LegalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java index ef41a86c..c79c42a2 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -68,7 +68,7 @@ public void sequentialWorkflow() { } @Test - @DisplayName("Looping agents via DSL.loop(...)") // TODO maxIterations(5) + @DisplayName("Looping agents via DSL.loop(...)") public void loopWorkflow() { var scorer = AgentsUtils.newStyleScorer(); @@ -103,6 +103,48 @@ public void loopWorkflow() { assertThat(result).containsKey("story"); } + @Test + @DisplayName("Looping agents via DSL.loop(...)") + public void loopWorkflowWithMaxIterations() { + var scorer = AgentsUtils.newStyleScorer(); + var editor = AgentsUtils.newStyleEditor(); + + Workflow wf = + AgentWorkflowBuilder.workflow("maxFlow") + .tasks( + d -> + d.loop( + "limit", + l -> + l.maxIterations(5) + .exitCondition(c -> c.readState("score", 0).doubleValue() >= 0.8) + .subAgents("sub", scorer, editor))) + .build(); + + List items = wf.getDo(); + assertThat(items).hasSize(1); + + var fn = (ForTaskFunction) items.get(0).getTask().getForTask(); + assertThat(fn.getDo()).isNotNull(); + assertThat(fn.getDo()).hasSize(2); + fn.getDo() + .forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); + + Map input = + Map.of( + "story", "dragons and wizards", + "style", "comedy"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("story"); + } + @Test @DisplayName("Parallel agents via DSL.parallel(...)") public void parallelWorkflow() { @@ -115,7 +157,9 @@ public void parallelWorkflow() { assertThat(items).hasSize(1); var fork = items.get(0).getTask().getForkTask(); + // two branches created assertThat(fork.getFork().getBranches()).hasSize(2); + // branch names follow "branch-{index}-{name}" assertThat(fork.getFork().getBranches().get(0).getName()).isEqualTo("branch-0-fanout"); assertThat(fork.getFork().getBranches().get(1).getName()).isEqualTo("branch-1-fanout"); @@ -132,6 +176,50 @@ public void parallelWorkflow() { assertEquals("Fake conflict response", result.get("movies")); } + // TODO + @Test + @DisplayName("Conditional agents via choice(...)") + public void conditionalWorkflow() { + + var category = AgentsUtils.newCategoryRouter(); + var a1 = AgentsUtils.newMedicalExpert(); + var a2 = AgentsUtils.newTechnicalExpert(); + var a3 = AgentsUtils.newLegalExpert(); + } + + // TODO + @Test + @DisplayName("Error handling with agents") + public void errorHandling() { + var a1 = AgentsUtils.newCreativeWriter(); + var a2 = AgentsUtils.newAudienceEditor(); + var a3 = AgentsUtils.newStyleEditor(); + + Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build(); + + List items = wf.getDo(); + assertThat(items).hasSize(3); + + assertThat(items.get(0).getName()).isEqualTo("process-0"); + assertThat(items.get(1).getName()).isEqualTo("process-1"); + assertThat(items.get(2).getName()).isEqualTo("process-2"); + items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); + + Map input = + Map.of( + "style", "fantasy", + "audience", "young adults"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("story"); + } + @Test @DisplayName("Human in the loop") public void humanInTheLoop() { @@ -149,8 +237,7 @@ public void humanInTheLoop() { var a1 = AgentsUtils.newAstrologyAgent(); - Workflow wf = - workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, humanInTheLoop)).build(); + Workflow wf = workflow("seqFlow").sequence("process", a1, humanInTheLoop).build(); assertThat(wf.getDo()).hasSize(2); From 254f8bd29e05ef422bda280a8f109b709ce0a2f0 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 23 Sep 2025 16:21:18 -0700 Subject: [PATCH 4/7] more examples Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 62 +++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java index c79c42a2..2523fd5d 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -16,11 +16,15 @@ package io.serverlessworkflow.fluent.agentic; import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.tasks; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.workflow.HumanInTheLoop; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.CallTaskJava; @@ -176,17 +180,6 @@ public void parallelWorkflow() { assertEquals("Fake conflict response", result.get("movies")); } - // TODO - @Test - @DisplayName("Conditional agents via choice(...)") - public void conditionalWorkflow() { - - var category = AgentsUtils.newCategoryRouter(); - var a1 = AgentsUtils.newMedicalExpert(); - var a2 = AgentsUtils.newTechnicalExpert(); - var a3 = AgentsUtils.newLegalExpert(); - } - // TODO @Test @DisplayName("Error handling with agents") @@ -220,6 +213,53 @@ public void errorHandling() { assertThat(result).containsKey("story"); } + @SuppressWarnings("unchecked") + @Test + @DisplayName("Conditional agents via choice(...)") + public void conditionalWorkflow() { + + var category = AgentsUtils.newCategoryRouter(); + var a1 = AgentsUtils.newMedicalExpert(); + var a2 = AgentsUtils.newTechnicalExpert(); + var a3 = AgentsUtils.newLegalExpert(); + + Workflow wf = + workflow("conditional") + .sequence("process", category) + .tasks( + t -> + t.switchCase( + p -> + p.onPredicate( + item -> + item.when( + m -> + "unknown" + .equals( + ((Map) m).get("category"))) + .then(FlowDirectiveEnum.END)))) + .tasks( + doTasks( + conditional( + m -> "medical".equals(((Map) m).get("category")), a1), + conditional( + m -> "technical".equals(((Map) m).get("category")), a2), + conditional( + m -> "legal".equals(((Map) m).get("category")), a3))) + .build(); + + Map input = Map.of("question", "What is the best treatment for a common cold?"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("response"); + } + @Test @DisplayName("Human in the loop") public void humanInTheLoop() { From c61d4966ed6e43f18514f17881e62a6a2e55b7e0 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 23 Sep 2025 17:42:32 -0700 Subject: [PATCH 5/7] added README.md Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 16 ++++------------ .../serverlessworkflow/fluent/agentic/README.md | 10 ++++++++++ 2 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java index 2523fd5d..9705b051 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -180,7 +180,6 @@ public void parallelWorkflow() { assertEquals("Fake conflict response", result.get("movies")); } - // TODO @Test @DisplayName("Error handling with agents") public void errorHandling() { @@ -232,20 +231,13 @@ public void conditionalWorkflow() { p -> p.onPredicate( item -> - item.when( - m -> - "unknown" - .equals( - ((Map) m).get("category"))) + item.when(Agents.RequestCategory.UNKNOWN::equals) .then(FlowDirectiveEnum.END)))) .tasks( doTasks( - conditional( - m -> "medical".equals(((Map) m).get("category")), a1), - conditional( - m -> "technical".equals(((Map) m).get("category")), a2), - conditional( - m -> "legal".equals(((Map) m).get("category")), a3))) + conditional(Agents.RequestCategory.MEDICAL::equals, a1), + conditional(Agents.RequestCategory.TECHNICAL::equals, a2), + conditional(Agents.RequestCategory.LEGAL::equals, a3))) .build(); Map input = Map.of("question", "What is the best treatment for a common cold?"); diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md new file mode 100644 index 00000000..34d9f270 --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md @@ -0,0 +1,10 @@ +# Implementation of Scenarios from [LangChain4j Agents Tutorials](https://docs.langchain4j.dev/tutorials/agents/) for CNCF Workflow Java DSL + +| Scenario | LangChain4j Reference | CNCF Workflow Java DSL Implementation | +|-----------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Sequential workflow | [Sequential workflow](https://docs.langchain4j.dev/tutorials/agents/#sequential-workflow) | [LC4JEquivalenceIT.java#L43](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L43) | +| Loop workflow | [Loop workflow](https://docs.langchain4j.dev/tutorials/agents/#loop-workflow) | [LC4JEquivalenceIT.java#L76](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L76)
[LC4JEquivalenceIT.java#L112](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L112) | +| Parallel workflow | [Parallel workflow](https://docs.langchain4j.dev/tutorials/agents/#parallel-workflow) | [LC4JEquivalenceIT.java#L154](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L154) | +| Conditional workflow | [Conditional workflow](https://docs.langchain4j.dev/tutorials/agents/#conditional-workflow) | [LC4JEquivalenceIT.java#L218](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L218) | +| Error handling | [Error handling](https://docs.langchain4j.dev/tutorials/agents/#error-handling) | [LC4JEquivalenceIT.java#L185](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L185) | +| Human-in-the-loop | [Human-in-the-loop](https://docs.langchain4j.dev/tutorials/agents/#human-in-the-loop) | [LC4JEquivalenceIT.java#L257](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L257) | From 9cab2743732d96ffa4512691e3d4f28ce03f6c79 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 24 Sep 2025 11:18:23 -0700 Subject: [PATCH 6/7] post review Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 69 ++-- .../fluent/agentic/README.md | 337 +++++++++++++++++- 2 files changed, 360 insertions(+), 46 deletions(-) diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java index 9705b051..7b609d78 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -18,13 +18,14 @@ import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.loop; import static io.serverlessworkflow.fluent.spec.dsl.DSL.tasks; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.scope.AgenticScope; import dev.langchain4j.agentic.workflow.HumanInTheLoop; -import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.CallTaskJava; @@ -33,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -41,11 +43,14 @@ public class LC4JEquivalenceIT { @Test @DisplayName("Sequential agents via DSL.sequence(...)") public void sequentialWorkflow() { - var a1 = AgentsUtils.newCreativeWriter(); - var a2 = AgentsUtils.newAudienceEditor(); - var a3 = AgentsUtils.newStyleEditor(); + var creativeWriter = AgentsUtils.newCreativeWriter(); + var audienceEditor = AgentsUtils.newAudienceEditor(); + var styleEditor = AgentsUtils.newStyleEditor(); - Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build(); + Workflow wf = + workflow("seqFlow") + .tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)) + .build(); List items = wf.getDo(); assertThat(items).hasSize(3); @@ -113,17 +118,10 @@ public void loopWorkflowWithMaxIterations() { var scorer = AgentsUtils.newStyleScorer(); var editor = AgentsUtils.newStyleEditor(); + Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; + Workflow wf = - AgentWorkflowBuilder.workflow("maxFlow") - .tasks( - d -> - d.loop( - "limit", - l -> - l.maxIterations(5) - .exitCondition(c -> c.readState("score", 0).doubleValue() >= 0.8) - .subAgents("sub", scorer, editor))) - .build(); + AgentWorkflowBuilder.workflow("retryFlow").tasks(loop(until, scorer, 5, editor)).build(); List items = wf.getDo(); assertThat(items).hasSize(1); @@ -152,10 +150,10 @@ public void loopWorkflowWithMaxIterations() { @Test @DisplayName("Parallel agents via DSL.parallel(...)") public void parallelWorkflow() { - var a1 = AgentsUtils.newFoodExpert(); - var a2 = AgentsUtils.newMovieExpert(); + var foodExpert = AgentsUtils.newFoodExpert(); + var movieExpert = AgentsUtils.newMovieExpert(); - Workflow wf = workflow("forkFlow").parallel("fanout", a1, a2).build(); + Workflow wf = workflow("forkFlow").parallel("fanout", foodExpert, movieExpert).build(); List items = wf.getDo(); assertThat(items).hasSize(1); @@ -183,11 +181,14 @@ public void parallelWorkflow() { @Test @DisplayName("Error handling with agents") public void errorHandling() { - var a1 = AgentsUtils.newCreativeWriter(); - var a2 = AgentsUtils.newAudienceEditor(); - var a3 = AgentsUtils.newStyleEditor(); + var creativeWriter = AgentsUtils.newCreativeWriter(); + var audienceEditor = AgentsUtils.newAudienceEditor(); + var styleEditor = AgentsUtils.newStyleEditor(); - Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build(); + Workflow wf = + workflow("seqFlow") + .tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)) + .build(); List items = wf.getDo(); assertThat(items).hasSize(3); @@ -218,26 +219,18 @@ public void errorHandling() { public void conditionalWorkflow() { var category = AgentsUtils.newCategoryRouter(); - var a1 = AgentsUtils.newMedicalExpert(); - var a2 = AgentsUtils.newTechnicalExpert(); - var a3 = AgentsUtils.newLegalExpert(); + var medicalExpert = AgentsUtils.newMedicalExpert(); + var technicalExpert = AgentsUtils.newTechnicalExpert(); + var legalExpert = AgentsUtils.newLegalExpert(); Workflow wf = workflow("conditional") .sequence("process", category) - .tasks( - t -> - t.switchCase( - p -> - p.onPredicate( - item -> - item.when(Agents.RequestCategory.UNKNOWN::equals) - .then(FlowDirectiveEnum.END)))) .tasks( doTasks( - conditional(Agents.RequestCategory.MEDICAL::equals, a1), - conditional(Agents.RequestCategory.TECHNICAL::equals, a2), - conditional(Agents.RequestCategory.LEGAL::equals, a3))) + conditional(Agents.RequestCategory.MEDICAL::equals, medicalExpert), + conditional(Agents.RequestCategory.TECHNICAL::equals, technicalExpert), + conditional(Agents.RequestCategory.LEGAL::equals, legalExpert))) .build(); Map input = Map.of("question", "What is the best treatment for a common cold?"); @@ -267,9 +260,9 @@ public void humanInTheLoop() { .responseReader(() -> "piscis") .build(); - var a1 = AgentsUtils.newAstrologyAgent(); + var astrologyAgent = AgentsUtils.newAstrologyAgent(); - Workflow wf = workflow("seqFlow").sequence("process", a1, humanInTheLoop).build(); + Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, humanInTheLoop).build(); assertThat(wf.getDo()).hasSize(2); diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md index 34d9f270..1c72d4d4 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md @@ -1,10 +1,331 @@ # Implementation of Scenarios from [LangChain4j Agents Tutorials](https://docs.langchain4j.dev/tutorials/agents/) for CNCF Workflow Java DSL -| Scenario | LangChain4j Reference | CNCF Workflow Java DSL Implementation | -|-----------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Sequential workflow | [Sequential workflow](https://docs.langchain4j.dev/tutorials/agents/#sequential-workflow) | [LC4JEquivalenceIT.java#L43](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L43) | -| Loop workflow | [Loop workflow](https://docs.langchain4j.dev/tutorials/agents/#loop-workflow) | [LC4JEquivalenceIT.java#L76](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L76)
[LC4JEquivalenceIT.java#L112](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L112) | -| Parallel workflow | [Parallel workflow](https://docs.langchain4j.dev/tutorials/agents/#parallel-workflow) | [LC4JEquivalenceIT.java#L154](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L154) | -| Conditional workflow | [Conditional workflow](https://docs.langchain4j.dev/tutorials/agents/#conditional-workflow) | [LC4JEquivalenceIT.java#L218](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L218) | -| Error handling | [Error handling](https://docs.langchain4j.dev/tutorials/agents/#error-handling) | [LC4JEquivalenceIT.java#L185](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L185) | -| Human-in-the-loop | [Human-in-the-loop](https://docs.langchain4j.dev/tutorials/agents/#human-in-the-loop) | [LC4JEquivalenceIT.java#L257](https://github.com/treblereel/serverless-workflow-sdk-java/blob/langchain_examples/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java#L257) | +# Sequential workflow +### Common part: +```java +public interface AudienceEditor { + + @UserMessage(""" + You are a professional editor. + Analyze and rewrite the following story to better align + with the target audience of {{audience}}. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given audience") + String editStory(@V("story") String story, @V("audience") String audience); +} + +interface CreativeWriter { + + @UserMessage(""" + You are a creative writer. + Generate a draft of a story no more than + 3 sentences long around the given topic. + Return only the story and nothing else. + The topic is {{topic}}. + """) + @Agent("Generates a story based on the given topic") + String generateStory(@V("topic") String topic); +} + +public interface StyleEditor { + + @UserMessage(""" + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); +} + +CreativeWriter creativeWriter = AgenticServices + .agentBuilder(CreativeWriter.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +AudienceEditor audienceEditor = AgenticServices + .agentBuilder(AudienceEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +StyleEditor styleEditor = AgenticServices + .agentBuilder(StyleEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +Map input = Map.of( + "topic", "dragons and wizards", + "style", "fantasy", + "audience", "young adults" +); +``` + +### LangChain4j +```java +UntypedAgent novelCreator = AgenticServices + .sequenceBuilder() + .subAgents(creativeWriter, audienceEditor, styleEditor) + .outputName("story") + .build(); + +String story = (String) novelCreator.invoke(input); +``` + +### Serverless Workflow + +```java +Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)).build(); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + + +### Loop workflow +### Common part: +```java + interface StyleEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); +} + +interface StyleScorer { + + @UserMessage( + """ + You are a critical reviewer. + Give a review score between 0.0 and 1.0 for the following + story based on how well it aligns with the style '{{style}}'. + Return only the score and nothing else. + + The story is: "{{story}}" + """) + @Agent("Scores a story based on how well it aligns with a given style") + double scoreStyle(@V("story") String story, @V("style") String style); +} + +StyleEditor styleEditor = AgenticServices + .agentBuilder(StyleEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +StyleScorer styleScorer = AgenticServices + .agentBuilder(StyleScorer.class) + .chatModel(BASE_MODEL) + .outputName("score") + .build(); + + +``` + +### LangChain4j +```java +StyledWriter styledWriter = AgenticServices + .sequenceBuilder(StyledWriter.class) + .subAgents(creativeWriter, styleReviewLoop) + .outputName("story") + .build(); + +String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy"); +``` + +### Serverless Workflow +```java +Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; + +Workflow wf = + AgentWorkflowBuilder.workflow("retryFlow") + .tasks(loop(until, scorer, 5, editor)) + .build(); + +Map input = + Map.of( + "story", "dragons and wizards", + "style", "comedy"); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + +### Parallel workflow +### Common part: +```java +public interface FoodExpert { + + @UserMessage(""" + You are a great evening planner. + Propose a list of 3 meals matching the given mood. + The mood is {{mood}}. + For each meal, just give the name of the meal. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMeal(@V("mood") String mood); +} + +public interface MovieExpert { + + @UserMessage(""" + You are a great evening planner. + Propose a list of 3 movies matching the given mood. + The mood is {mood}. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMovie(@V("mood") String mood); +} + +FoodExpert foodExpert = AgenticServices + .agentBuilder(FoodExpert.class) + .chatModel(BASE_MODEL) + .outputName("meals") + .build(); + +MovieExpert movieExpert = AgenticServices + .agentBuilder(MovieExpert.class) + .chatModel(BASE_MODEL) + .outputName("movies") + .build(); +``` + +### LangChain4j +```java +EveningPlannerAgent eveningPlannerAgent = AgenticServices + .parallelBuilder(EveningPlannerAgent.class) + .subAgents(foodExpert, movieExpert) + .executor(Executors.newFixedThreadPool(2)) + .outputName("plans") + .output(agenticScope -> { + List movies = agenticScope.readState("movies", List.of()); + List meals = agenticScope.readState("meals", List.of()); + + List moviesAndMeals = new ArrayList<>(); + for (int i = 0; i < movies.size(); i++) { + if (i >= meals.size()) { + break; + } + moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i))); + } + return moviesAndMeals; + }) + .build(); + +List plans = eveningPlannerAgent.plan("romantic"); +``` + +### Serverless Workflow +```java +Workflow wf = workflow("forkFlow").parallel("fanout", foodExpert, movieExpert).build(); + +Map input = Map.of("mood", "I am hungry and bored"); + +Map result; +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + +### Error handling +### Common part: +```java + +``` + +### LangChain4j +```java + +``` + +### Serverless Workflow + +```java + +``` + +### Human-in-the-loop +### Common part: +```java +public record HumanInTheLoop(Consumer requestWriter, Supplier responseReader) { + + @Agent("An agent that asks the user for missing information") + public String askUser(String request) { + requestWriter.accept(request); + return responseReader.get(); + } +} + +public interface AstrologyAgent { + @SystemMessage(""" + You are an astrologist that generates horoscopes based on the user's name and zodiac sign. + """) + @UserMessage(""" + Generate the horoscope for {{name}} who is a {{sign}}. + """) + @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") + String horoscope(@V("name") String name, @V("sign") String sign); +} + +AstrologyAgent astrologyAgent = AgenticServices + .agentBuilder(AstrologyAgent.class) + .chatModel(BASE_MODEL) + .build(); + +HumanInTheLoop humanInTheLoop = AgenticServices + .humanInTheLoopBuilder() + .description("An agent that asks the zodiac sign of the user") + .outputName("sign") + .requestWriter(request -> { + System.out.println(request); + System.out.print("> "); + }) + .responseReader(() -> System.console().readLine()) + .build(); +``` + +### LangChain4j +```java +SupervisorAgent horoscopeAgent = AgenticServices + .supervisorBuilder() + .chatModel(PLANNER_MODEL) + .subAgents(astrologyAgent, humanInTheLoop) + .build(); + +horoscopeAgent.invoke("My name is Mario. What is my horoscope?") +``` + +### Serverless Workflow + +```java +Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, humanInTheLoop).build(); + +Map input = Map.of("request", "My name is Mario. What is my horoscope?"); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` \ No newline at end of file From f6c70ac1b3440b1ab0a459210380cb5ad7b07b39 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Wed, 24 Sep 2025 16:06:17 -0700 Subject: [PATCH 7/7] updated sequence demo Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 9 +++------ .../java/io/serverlessworkflow/fluent/agentic/README.md | 8 +++----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java index 7b609d78..492b373c 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -18,8 +18,6 @@ import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.loop; -import static io.serverlessworkflow.fluent.spec.dsl.DSL.tasks; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -49,7 +47,7 @@ public void sequentialWorkflow() { Workflow wf = workflow("seqFlow") - .tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)) + .sequence("process", creativeWriter, audienceEditor, styleEditor) .build(); List items = wf.getDo(); @@ -120,8 +118,7 @@ public void loopWorkflowWithMaxIterations() { Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; - Workflow wf = - AgentWorkflowBuilder.workflow("retryFlow").tasks(loop(until, scorer, 5, editor)).build(); + Workflow wf = workflow("retryFlow").loop(until, scorer, 5, editor).build(); List items = wf.getDo(); assertThat(items).hasSize(1); @@ -187,7 +184,7 @@ public void errorHandling() { Workflow wf = workflow("seqFlow") - .tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)) + .sequence("process", creativeWriter, audienceEditor, styleEditor) .build(); List items = wf.getDo(); diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md index 1c72d4d4..a9c2e469 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md @@ -80,7 +80,7 @@ String story = (String) novelCreator.invoke(input); ### Serverless Workflow ```java -Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", creativeWriter, audienceEditor, styleEditor)).build(); +Workflow wf = workflow("seqFlow").sequence("process", creativeWriter, audienceEditor, styleEditor).build(); try (WorkflowApplication app = WorkflowApplication.builder().build()) { String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); @@ -151,10 +151,8 @@ String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy") ```java Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; -Workflow wf = - AgentWorkflowBuilder.workflow("retryFlow") - .tasks(loop(until, scorer, 5, editor)) - .build(); +Workflow wf = workflow("retryFlow").loop(until, scorer, 5, editor).build(); + Map input = Map.of(