From ae2cd081f34eca956afcbb16b7c3293d20a44738 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 25 Sep 2025 14:24:15 -0700 Subject: [PATCH 1/6] add better docs about Langchain4g/ServerlessWorkflow Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/dsl/AgenticDSL.java | 2 +- .../fluent/agentic/LC4JEquivalenceIT.java | 8 +- .../fluent/agentic/README.md | 254 ++++++++++++------ 3 files changed, 169 insertions(+), 95 deletions(-) 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 ccf77f374..d91fb86cf 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 @@ -122,7 +122,7 @@ public static AgentTaskConfigurer loop(Predicate exitCondition, Ob } public static AgentTaskConfigurer loop( - Predicate exitCondition, int maxIterations, Object... agents) { + int maxIterations, Predicate exitCondition, Object... agents) { return list -> list.loop( l -> l.subAgents(agents).exitCondition(exitCondition).maxIterations(maxIterations)); 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 59dd8a0d4..3c911859d 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,6 +18,7 @@ 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 org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,10 +46,7 @@ public void sequentialWorkflow() { var audienceEditor = AgentsUtils.newAudienceEditor(); var styleEditor = AgentsUtils.newStyleEditor(); - Workflow wf = - workflow("seqFlow") - .sequence("process", creativeWriter, audienceEditor, styleEditor) - .build(); + Workflow wf = workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor).build(); List items = wf.getDo(); assertThat(items).hasSize(3); @@ -119,7 +117,7 @@ public void loopWorkflowWithMaxIterations() { Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; - Workflow wf = workflow("retryFlow").loop(until, scorer, 5, editor).build(); + Workflow wf = workflow("retryFlow").tasks(loop(5, until, scorer, editor)).build(); List items = wf.getDo(); assertThat(items).hasSize(1); 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 a9c2e469c..3a635829f 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 @@ -2,6 +2,8 @@ # Sequential workflow ### Common part: +
Click to expand + ```java public interface AudienceEditor { @@ -65,33 +67,53 @@ Map input = Map.of( "audience", "young adults" ); ``` - -### LangChain4j -```java -UntypedAgent novelCreator = AgenticServices - .sequenceBuilder() - .subAgents(creativeWriter, audienceEditor, styleEditor) - .outputName("story") - .build(); +
+ + + + + + + + -```java -Workflow wf = workflow("seqFlow").sequence("process", creativeWriter, audienceEditor, styleEditor).build(); + + +
LangChain4jServerless Workflow
+
+UntypedAgent novelCreator = AgenticServices
+    .sequenceBuilder()
+    .subAgents(creativeWriter, audienceEditor, styleEditor)
+    .outputName("story")
+    .build();
 
 String story = (String) novelCreator.invoke(input);
-```
 
-### Serverless Workflow
+
+
+
+
+Workflow wf = workflow("seqFlow")
+    .sequence(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: +
Click to expand + ```java interface StyleEditor { @@ -136,38 +158,58 @@ StyleScorer styleScorer = AgenticServices ``` -### LangChain4j -```java +
+ + + + + + + + + + +
LangChain4jServerless Workflow
+
+
+ 
+ 
 StyledWriter styledWriter = AgenticServices
-        .sequenceBuilder(StyledWriter.class)
-        .subAgents(creativeWriter, styleReviewLoop)
-        .outputName("story")
-        .build();
+    .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 = workflow("retryFlow").loop(until, scorer, 5, editor).build();
+
+
+
+
+Map<String, Object> input =  Map.of("story", "dragons and wizards","style", "comedy");
+Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8;
 
-Map input =
-        Map.of(
-                "story", "dragons and wizards",
-                "style", "comedy");
-
+Workflow wf = workflow("retryFlow")
+    .loop(until, scorer, editor)
+    .build();
+ 
+ 
+ 
 try (WorkflowApplication app = WorkflowApplication.builder().build()) {
-  String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow();
+    String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow();
 } catch (Exception e) {
-  throw new RuntimeException("Workflow execution failed", e);
+    throw new RuntimeException("Workflow execution failed", e);
 }
-```
+
+
+
+
### Parallel workflow ### Common part: +
Click to expand + ```java public interface FoodExpert { @@ -206,65 +248,81 @@ MovieExpert movieExpert = AgenticServices .outputName("movies") .build(); ``` - -### LangChain4j -```java +
+ + + + + + + + + + + + +
LangChain4jServerless Workflow
+
+
 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();
+    .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();
+
+
+
+
+
+Workflow wf = workflow("forkFlow")
+    .parallel(foodExpert, movieExpert)
+    .build();
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+ 
+Map<String, Object> input = Map.of("mood", "I am hungry and bored");
 
-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();
+    Map result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
 } catch (Exception e) {
-   throw new RuntimeException("Workflow execution failed", e);
+    throw new RuntimeException("Workflow execution failed", e);
 }
-```
-
-### Error handling
-### Common part:
-```java
-
-```
-
-### LangChain4j
-```java
-
-```
 
-### Serverless Workflow
+
+
+
-```java - -``` ### Human-in-the-loop ### Common part: +
Click to expand + ```java public record HumanInTheLoop(Consumer requestWriter, Supplier responseReader) { @@ -302,9 +360,17 @@ HumanInTheLoop humanInTheLoop = AgenticServices .responseReader(() -> System.console().readLine()) .build(); ``` - -### LangChain4j -```java +
+ + + + + + + + -```java -Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, humanInTheLoop).build(); + + +
LangChain4jServerless Workflow
+
+
 SupervisorAgent horoscopeAgent = AgenticServices
         .supervisorBuilder()
         .chatModel(PLANNER_MODEL)
@@ -312,18 +378,28 @@ SupervisorAgent horoscopeAgent = AgenticServices
         .build();
 
 horoscopeAgent.invoke("My name is Mario. What is my horoscope?")
-```
 
-### Serverless Workflow
+
+
+
+
+
+Workflow wf = workflow("seqFlow")
+    .sequence(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();
+    String result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
 } catch (Exception e) {
-  throw new RuntimeException("Workflow execution failed", e);
+    throw new RuntimeException("Workflow execution failed", e);
 }
-```
\ No newline at end of file
+
+
+
+
\ No newline at end of file From 71175c1ce6ac2d996df8ce84263cb5c141021779 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 25 Sep 2025 14:50:05 -0700 Subject: [PATCH 2/6] no try-catch-blocks Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/README.md | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) 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 3a635829f..ea4d8fe12 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 @@ -97,11 +97,7 @@ String story = (String) novelCreator.invoke(input);     -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); -} +String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); @@ -194,11 +190,7 @@ Workflow wf = workflow("retryFlow")       -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); -} +String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); @@ -304,13 +296,10 @@ Workflow wf = workflow("forkFlow")       +  Map<String, Object> input = Map.of("mood", "I am hungry and bored"); -try (WorkflowApplication app = WorkflowApplication.builder().build()) { - Map result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); -} catch (Exception e) { - throw new RuntimeException("Workflow execution failed", e); -} +Map result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); @@ -390,13 +379,9 @@ Workflow wf = workflow("seqFlow") .sequence(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); -} +  +  +String result = app.workflowDefinition(wf).instance("My name is Mario. What is my horoscope?").start().get().asMap().orElseThrow(); From c1279633de37f81ad4a9b17bda8f0106caa82add Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Tue, 21 Oct 2025 11:44:07 -0700 Subject: [PATCH 3/6] docs and tests are updated Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/langchain4j/Agents.java | 194 ------------ .../agentic/langchain4j/WorkflowAgentsIT.java | 137 --------- .../agentic/AgenticWorkflowHelperIT.java | 233 +++++++++++++++ .../fluent/agentic/Agents.java | 16 + .../fluent/agentic/LC4JEquivalenceIT.java | 277 ------------------ .../fluent/agentic/README.md | 142 +++++---- 6 files changed, 328 insertions(+), 671 deletions(-) create mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java index f2b6359d2..730ef65f0 100644 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java +++ b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java @@ -15,140 +15,12 @@ */ package io.serverlessworkflow.fluent.agentic.langchain4j; -import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agentic.Agent; -import dev.langchain4j.agentic.scope.AgenticScopeAccess; -import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; -import java.util.List; public class Agents { - public interface ExpertRouterAgent { - - @Agent - String ask(@V("request") String request); - } - - public interface ExpertRouterAgentWithMemory extends AgenticScopeAccess { - - @Agent - String ask(@MemoryId String memoryId, @V("request") String request); - } - - public 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("Categorize a user request") - RequestCategory classify(@V("request") String request); - } - - public enum RequestCategory { - LEGAL, - MEDICAL, - TECHNICAL, - UNKNOWN - } - - public interface RouterAgent { - - @UserMessage( - """ - Analyze the following user request and categorize it as 'legal', 'medical' or 'technical', - then forward the request as it is to the corresponding expert provided as a tool. - Finally return the answer that you received from the expert without any modification. - - The user request is: '{{it}}'. - """) - @Agent - String askToExpert(String request); - } - - public interface MedicalExpert { - - @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 {{request}}. - """) - @Tool("A medical expert") - @Agent("A medical expert") - String medical(@V("request") String request); - } - - public interface MedicalExpertWithMemory { - - @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 {{request}}. - """) - @Tool("A medical expert") - @Agent("A medical expert") - String medical(@MemoryId String memoryId, @V("request") String request); - } - - public interface LegalExpert { - - @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 {{request}}. - """) - @Tool("A legal expert") - @Agent("A legal expert") - String legal(@V("request") String request); - } - - public interface LegalExpertWithMemory { - - @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 {{request}}. - """) - @Tool("A legal expert") - @Agent("A legal expert") - String legal(@MemoryId String memoryId, @V("request") String request); - } - - public interface TechnicalExpert { - - @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 {{request}}. - """) - @Tool("A technical expert") - @Agent("A technical expert") - String technical(@V("request") String request); - } - - public interface TechnicalExpertWithMemory { - - @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 {{request}}. - """) - @Tool("A technical expert") - @Agent("A technical expert") - String technical(@MemoryId String memoryId, @V("request") String request); - } - public interface CreativeWriter { @UserMessage( @@ -188,70 +60,4 @@ public interface StyleEditor { String editStory(@V("story") String story, @V("style") String style); } - public 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("Score a story based on how well it aligns with a given style") - double scoreStyle(@V("story") String story, @V("style") String style); - } - - public interface StyleReviewLoop { - - @Agent("Review the given story to ensure it aligns with the specified style") - String scoreAndReview(@V("story") String story, @V("style") String style); - } - - public interface StyledWriter extends AgenticScopeAccess { - - @Agent - String writeStoryWithStyle(@V("topic") String topic, @V("style") String style); - } - - 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); - } - - public record EveningPlan(String movie, String meal) {} - - public interface EveningPlannerAgent { - - @Agent - List plan(@V("mood") String mood); - } - - public interface HoroscopeAgent { - - @Agent - String invoke(@V("name") String name); - } } diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java index aea64fabb..d0d68f309 100644 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java +++ b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java @@ -15,22 +15,10 @@ */ package io.serverlessworkflow.fluent.agentic.langchain4j; -import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAstrologyAgent; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAudienceEditor; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newCreativeWriter; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newFoodExpert; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newMovieExpert; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleEditor; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleScorer; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newSummaryStory; -import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.*; import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.AudienceEditor; import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.CreativeWriter; import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.StyleEditor; import static io.serverlessworkflow.fluent.agentic.langchain4j.Models.BASE_MODEL; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.spy; @@ -38,15 +26,8 @@ import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.UntypedAgent; -import dev.langchain4j.agentic.scope.AgenticScope; import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder; -import io.serverlessworkflow.fluent.agentic.AgenticWorkflow; -import io.serverlessworkflow.fluent.agentic.AgentsUtils; -import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.IntStream; import org.junit.jupiter.api.Test; public class WorkflowAgentsIT { @@ -97,122 +78,4 @@ void sequential_agents_tests() { verify(styleEditor).editStory(any(), eq("fantasy")); } - @Test - public void sequenceHelperTest() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - - AgentsUtils.NovelCreator novelCreator = - AgenticWorkflow.of(AgentsUtils.NovelCreator.class) - .flow(workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor)) - .build(); - - String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); - assertNotNull(story); - } - - @Test - public void agentAndSequenceHelperTest() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - - AgentsUtils.NovelCreator novelCreator = - AgenticWorkflow.of(AgentsUtils.NovelCreator.class) - .flow(workflow("seqFlow").agent(creativeWriter).sequence(audienceEditor, styleEditor)) - .build(); - - String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); - assertNotNull(story); - } - - @Test - public void agentAndSequenceAndAgentHelperTest() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - var summaryStory = newSummaryStory(); - - AgentsUtils.NovelCreator novelCreator = - AgenticWorkflow.of(AgentsUtils.NovelCreator.class) - .flow( - workflow("seqFlow") - .agent(creativeWriter) - .sequence(audienceEditor, styleEditor) - .agent(summaryStory)) - .build(); - - String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); - assertNotNull(story); - } - - @Test - public void parallelWorkflow() { - var foodExpert = newFoodExpert(); - var movieExpert = newMovieExpert(); - - Function> planEvening = - input -> { - List movies = (List) input.readState("movies"); - List meals = (List) input.readState("meals"); - - int max = Math.min(movies.size(), meals.size()); - return IntStream.range(0, max) - .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) - .toList(); - }; - - EveningPlannerAgent eveningPlannerAgent = - AgenticWorkflow.of(EveningPlannerAgent.class) - .flow(workflow("parallelFlow").parallel(foodExpert, movieExpert).outputAs(planEvening)) - .build(); - List result = eveningPlannerAgent.plan("romantic"); - assertEquals(3, result.size()); - } - - @Test - public void loopTest() { - var creativeWriter = newCreativeWriter(); - var scorer = newStyleScorer(); - var editor = newStyleEditor(); - - Predicate until = s -> s.readState("score", 0.0) >= 0.8; - - StyledWriter styledWriter = - AgenticWorkflow.of(StyledWriter.class) - .flow(workflow("loopFlow").agent(creativeWriter).loop(until, scorer, editor)) - .build(); - - String story = styledWriter.writeStoryWithStyle("dragons and wizards", "fantasy"); - assertNotNull(story); - } - - @Test - public void humanInTheLoop() { - var astrologyAgent = newAstrologyAgent(); - - var askSign = - new Function() { - @Override - public AgenticScope apply(AgenticScope holder) { - System.out.println("What's your star sign?"); - // var sign = System.console().readLine(); - holder.writeState("sign", "piscis"); - return holder; - } - }; - - String result = - AgenticWorkflow.of(Agents.HoroscopeAgent.class) - .flow( - workflow("humanInTheLoop") - .inputFrom(askSign) - // .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too - .agent(astrologyAgent)) - .build() - .invoke("My name is Mario. What is my horoscope?"); - - assertNotNull(result); - } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java new file mode 100644 index 000000000..a8e70613f --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java @@ -0,0 +1,233 @@ +/* + * 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 io.serverlessworkflow.fluent.agentic.Agents.*; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.*; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAstrologyAgent; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAudienceEditor; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newCreativeWriter; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newFoodExpert; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newMovieExpert; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleEditor; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleScorer; +import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newSummaryStory; +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 org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import dev.langchain4j.agentic.scope.AgenticScope; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.impl.WorkflowApplication; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class AgenticWorkflowHelperIT { + + @Test + @DisplayName("Sequential agents via DSL.sequence(...)") + public void sequentialWorkflow() { + var creativeWriter = newCreativeWriter(); + var audienceEditor = newAudienceEditor(); + var styleEditor = newStyleEditor(); + + NovelCreator novelCreator = AgenticWorkflow.of(NovelCreator.class) + .flow(workflow("seqFlow") + .sequence(creativeWriter, audienceEditor, styleEditor)) + .build(); + + String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); + assertNotNull(story); + } + + @Test + @DisplayName("Parallel agents via DSL.parallel(...)") + public void parallelWorkflow() { + var foodExpert = newFoodExpert(); + var movieExpert = newMovieExpert(); + + Function> planEvening = input -> { + List movies = (List) input.readState("movies"); + List meals = (List) input.readState("meals"); + + int max = Math.min(movies.size(), meals.size()); + return IntStream.range(0, max) + .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) + .toList(); + }; + + EveningPlannerAgent eveningPlannerAgent = AgenticWorkflow.of(EveningPlannerAgent.class) + .flow(workflow("parallelFlow") + .parallel(foodExpert, movieExpert).outputAs(planEvening)) + .build(); + List result = eveningPlannerAgent.plan("romantic"); + assertEquals(3, result.size()); + } + + @Test + @DisplayName("Loop test with condition") + public void loopTest() { + var creativeWriter = newCreativeWriter(); + var styleScorer = newStyleScorer(); + var styleEditor = newStyleEditor(); + + Predicate until = s -> s.readState("score", 0.0) >= 0.8; + + StyledWriter styledWriter = AgenticWorkflow.of(StyledWriter.class) + .flow(workflow("loopFlow") + .agent(creativeWriter) + .loop(until, styleScorer, styleEditor)) + .build(); + + String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy"); + assertNotNull(story); + } + + @Test + @DisplayName("Looping agents via DSL.loop(...)") + public void loopWorkflowWithMaxIterations() { + var creativeWriter = newCreativeWriter(); + var audienceEditor = newAudienceEditor(); + var styleEditor = newStyleEditor(); + var summaryStory = newSummaryStory(); + + NovelCreator novelCreator = + AgenticWorkflow.of(NovelCreator.class) + .flow( + workflow("seqFlow") + .agent(creativeWriter) + .sequence(audienceEditor, styleEditor) + .agent(summaryStory)) + .build(); + + String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); + assertNotNull(story); + } + + @Test + @DisplayName("Human in the loop") + public void humanInTheLoop() { + var astrologyAgent = newAstrologyAgent(); + + var askSign = + new Function() { + @Override + public AgenticScope apply(AgenticScope holder) { + System.out.println("What's your star sign?"); + // var sign = System.console().readLine(); + holder.writeState("sign", "piscis"); + return holder; + } + }; + + String result = + AgenticWorkflow.of(HoroscopeAgent.class) + .flow( + workflow("humanInTheLoop") + .inputFrom(askSign) + // .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too + .agent(astrologyAgent)) + .build() + .invoke("My name is Mario. What is my horoscope?"); + + assertNotNull(result); + } + + @Test + @DisplayName("Error handling with agents") + public void errorHandling() { + var creativeWriter = newCreativeWriter(); + var audienceEditor = newAudienceEditor(); + var styleEditor = newStyleEditor(); + + Workflow wf = + workflow("seqFlow") + .sequence("process", creativeWriter, audienceEditor, styleEditor) + .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"); + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Conditional agents via choice(...)") + public void conditionalWorkflow() { + + var category = newCategoryRouter(); + var medicalExpert = newMedicalExpert(); + var technicalExpert = newTechnicalExpert(); + var legalExpert = newLegalExpert(); + + Workflow wf = + workflow("conditional") + .sequence("process", category) + .tasks( + doTasks( + conditional(RequestCategory.MEDICAL::equals, medicalExpert), + conditional(RequestCategory.TECHNICAL::equals, technicalExpert), + conditional(RequestCategory.LEGAL::equals, legalExpert))) + .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"); + } + +} 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 d9c4bf478..902913d22 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 @@ -18,6 +18,7 @@ import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; +import dev.langchain4j.agentic.scope.AgenticScopeAccess; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; @@ -293,6 +294,12 @@ interface StyleScorer { double scoreStyle(@V("story") String story, @V("style") String style); } + interface StyledWriter extends AgenticScopeAccess { + + @Agent + String writeStoryWithStyle(@V("topic") String topic, @V("style") String style); + } + interface SummaryStory { @UserMessage( @@ -391,4 +398,13 @@ interface TechnicalExpert { @Tool("A technical expert") String technicalRequest(String request); } + + record EveningPlan(String movie, String meal) {} + + interface EveningPlannerAgent { + + @Agent + List plan(@V("mood") String mood); + } + } 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 deleted file mode 100644 index 3c911859d..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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 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 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.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 java.util.function.Predicate; -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 creativeWriter = AgentsUtils.newCreativeWriter(); - var audienceEditor = AgentsUtils.newAudienceEditor(); - var styleEditor = AgentsUtils.newStyleEditor(); - - Workflow wf = workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor).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(...)") - public void loopWorkflow() { - var creativeWriter = AgentsUtils.newCreativeWriter(); - var scorer = AgentsUtils.newStyleScorer(); - var editor = AgentsUtils.newStyleEditor(); - - Workflow wf = - AgentWorkflowBuilder.workflow("retryFlow") - .agent(creativeWriter) - .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("Looping agents via DSL.loop(...)") - public void loopWorkflowWithMaxIterations() { - var scorer = AgentsUtils.newStyleScorer(); - var editor = AgentsUtils.newStyleEditor(); - - Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; - - Workflow wf = workflow("retryFlow").tasks(loop(5, until, 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 foodExpert = AgentsUtils.newFoodExpert(); - var movieExpert = AgentsUtils.newMovieExpert(); - - Workflow wf = workflow("forkFlow").parallel("fanout", foodExpert, movieExpert).build(); - - List items = wf.getDo(); - 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"); - - 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("Error handling with agents") - public void errorHandling() { - var creativeWriter = AgentsUtils.newCreativeWriter(); - var audienceEditor = AgentsUtils.newAudienceEditor(); - var styleEditor = AgentsUtils.newStyleEditor(); - - Workflow wf = - workflow("seqFlow") - .sequence("process", creativeWriter, audienceEditor, styleEditor) - .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"); - } - - @SuppressWarnings("unchecked") - @Test - @DisplayName("Conditional agents via choice(...)") - public void conditionalWorkflow() { - - var category = AgentsUtils.newCategoryRouter(); - var medicalExpert = AgentsUtils.newMedicalExpert(); - var technicalExpert = AgentsUtils.newTechnicalExpert(); - var legalExpert = AgentsUtils.newLegalExpert(); - - Workflow wf = - workflow("conditional") - .sequence("process", category) - .tasks( - doTasks( - 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?"); - - 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() { - - 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 astrologyAgent = AgentsUtils.newAstrologyAgent(); - - Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, 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"); - } -} 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 ea4d8fe12..21b5fade9 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 @@ -91,13 +91,13 @@ String story = (String) novelCreator.invoke(input);
-Workflow wf = workflow("seqFlow")
-    .sequence(creativeWriter, audienceEditor, styleEditor)
-    .build();
+AgentsUtils.NovelCreator novelCreator = AgenticWorkflow.of(NovelCreator.class)
+            .flow(workflow("seqFlow")
+            .sequence(creativeWriter, audienceEditor, styleEditor))
+            .build();
  
  
-
-String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow();
+String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
 
 
 
@@ -164,14 +164,18 @@ StyleScorer styleScorer = AgenticServices
-
- 
- 
+UntypedAgent styleReviewLoop = AgenticServices
+        .loopBuilder()
+        .subAgents(styleScorer, styleEditor)
+        .maxIterations(5)
+        .exitCondition(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
+        .build();
+
 StyledWriter styledWriter = AgenticServices
-    .sequenceBuilder(StyledWriter.class)
-    .subAgents(creativeWriter, styleReviewLoop)
-    .outputName("story")
-    .build();
+        .sequenceBuilder(StyledWriter.class)
+        .subAgents(creativeWriter, styleReviewLoop)
+        .outputName("story")
+        .build();
 
 String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
 
@@ -181,16 +185,20 @@ String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy")
 
 
 
-Map<String, Object> input =  Map.of("story", "dragons and wizards","style", "comedy");
-Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8;
+Predicate until = s -> s.readState("score", 0.0) >= 0.8;
 
-Workflow wf = workflow("retryFlow")
-    .loop(until, scorer, editor)
-    .build();
+StyledWriter styledWriter = AgenticWorkflow.of(StyledWriter.class)
+            .flow(workflow("loopFlow")
+            .agent(creativeWriter)
+            .loop(until, styleScorer, styleEditor))
+            .build();
+ 
+ 
  
  
  
-String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow();
+
+String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
 
 
 
@@ -251,8 +259,7 @@ MovieExpert movieExpert = AgenticServices
-
-EveningPlannerAgent eveningPlannerAgent = AgenticServices
+EveningPlannerAgent eveningPlannerAgent = AgenticServices
     .parallelBuilder(EveningPlannerAgent.class)
     .subAgents(foodExpert, movieExpert)
     .executor(Executors.newFixedThreadPool(2))
@@ -279,28 +286,26 @@ List plans = eveningPlannerAgent.plan("romantic");
 
 
 
-
-Workflow wf = workflow("forkFlow")
-    .parallel(foodExpert, movieExpert)
-    .build();
- 
- 
- 
- 
- 
- 
- 
- 
- 
+Function<AgenticScope, List> planEvening = input -> {
+  List movies = (List) input.readState("movies");
+  List meals = (List)  input.readState("meals");
+  int max = Math.min(movies.size(), meals.size());
+  return IntStream.range(0, max)
+     .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i)))
+     .toList();
+};
+
+EveningPlannerAgent eveningPlannerAgent = AgenticWorkflow.of(EveningPlannerAgent.class)
+            .flow(workflow("parallelFlow")
+            .parallel(foodExpert, movieExpert)
+            .outputAs(planEvening))
+            .build();
  
  
  
  
  
-Map<String, Object> input = Map.of("mood", "I am hungry and bored");
-
-Map result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow();
-
+List result = eveningPlannerAgent.plan("romantic");
 
 
@@ -313,15 +318,6 @@ Map result = app.workflowDefinition(wf).instance(input).start().
Click to expand ```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. @@ -337,17 +333,6 @@ 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(); ```
@@ -359,7 +344,23 @@ HumanInTheLoop humanInTheLoop = AgenticServices
-
+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();
+    }
+}
+
+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();
+
 SupervisorAgent horoscopeAgent = AgenticServices
         .supervisorBuilder()
         .chatModel(PLANNER_MODEL)
@@ -374,15 +375,30 @@ horoscopeAgent.invoke("My name is Mario. What is my horoscope?")
 
 
 
-
-Workflow wf = workflow("seqFlow")
-    .sequence(astrologyAgent, humanInTheLoop)
-    .build();
+var askSign = new Function<AgenticScope, AgenticScope>() {
+  @Override
+  public AgenticScope apply(AgenticScope holder) {
+    System.out.println("What's your star sign?");
+    // var sign = System.console().readLine();
+    holder.writeState("sign", "piscis");
+    return holder;
+  }
+};
 
  
  
-String result = app.workflowDefinition(wf).instance("My name is Mario. What is my horoscope?").start().get().asMap().orElseThrow();
+ 
+ 
+ 
+ 
+ 
 
+String result = AgenticWorkflow.of(HoroscopeAgent.class)
+  .flow(workflow("humanInTheLoop")
+  .inputFrom(askSign)
+  .agent(astrologyAgent))
+  .build()
+  .invoke("My name is Mario. What is my horoscope?");
 
 
From f86a421632d0864b8c15334142ed91c136c19869 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 25 Sep 2025 18:26:38 -0700 Subject: [PATCH 4/6] we can do better Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 319 ++++++++++++++++++ 1 file changed, 319 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/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java new file mode 100644 index 000000000..56fe25294 --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -0,0 +1,319 @@ +/* + * 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 io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn; +import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.loop; +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.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.ArrayList; +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; + +public class LC4JEquivalenceIT { + + @Test + @DisplayName("Sequential agents via DSL.sequence(...)") + public void sequentialWorkflow() { + var creativeWriter = AgentsUtils.newCreativeWriter(); + var audienceEditor = AgentsUtils.newAudienceEditor(); + var styleEditor = AgentsUtils.newStyleEditor(); + + Workflow wf = workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor).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(...)") + public void loopWorkflow() { + var creativeWriter = AgentsUtils.newCreativeWriter(); + var styleScorer = AgentsUtils.newStyleScorer(); + var styleEditor = AgentsUtils.newStyleEditor(); + + Workflow wf = + AgentWorkflowBuilder.workflow("retryFlow") + .agent(creativeWriter) + .loop( + "reviewLoop", + c -> c.readState("score", 0).doubleValue() >= 0.8, + styleScorer, + styleEditor) + .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("Looping agents via DSL.loop(...)") + public void loopWorkflowWithMaxIterations() { + var scorer = AgentsUtils.newStyleScorer(); + var editor = AgentsUtils.newStyleEditor(); + + Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; + + Workflow wf = workflow("retryFlow").tasks(loop(5, until, 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"); + } + + public record EveningPlan(String movie, String meal) {} + + @Test + @DisplayName("Parallel agents via DSL.parallel(...)") + public void parallelWorkflow() { + var foodExpert = AgentsUtils.newFoodExpert(); + var movieExpert = AgentsUtils.newMovieExpert(); + + workflow("forkFlow") + .tasks( + d -> + d.parallel(foodExpert, movieExpert) + .callFn( + fn( + f -> { + Map> asMap = (Map>) f; + List result = new ArrayList<>(); + int max = + asMap.values().stream() + .map(List::size) + .min(Integer::compareTo) + .orElse(0); + for (int i = 0; i < max; i++) { + result.add( + new EveningPlan( + asMap.get("movies").get(i), asMap.get("meals").get(i))); + } + return result; + }))) + .build(); + + Workflow wf = workflow("forkFlow") + .tasks(d -> d + .parallel("fanout", foodExpert, movieExpert) + .callFn(fn((Map> m) -> { + var movies = m.getOrDefault("movies", List.of()); + var meals = m.getOrDefault("meals", List.of()); + return java.util.stream.IntStream + .range(0, Math.min(movies.size(), meals.size())) + .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) + .toList(); + })) + ).build(); + + List items = wf.getDo(); + 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"); + + 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("Error handling with agents") + public void errorHandling() { + var creativeWriter = AgentsUtils.newCreativeWriter(); + var audienceEditor = AgentsUtils.newAudienceEditor(); + var styleEditor = AgentsUtils.newStyleEditor(); + + Workflow wf = + workflow("seqFlow") + .sequence("process", creativeWriter, audienceEditor, styleEditor) + .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"); + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Conditional agents via choice(...)") + public void conditionalWorkflow() { + + var category = AgentsUtils.newCategoryRouter(); + var medicalExpert = AgentsUtils.newMedicalExpert(); + var technicalExpert = AgentsUtils.newTechnicalExpert(); + var legalExpert = AgentsUtils.newLegalExpert(); + + Workflow wf = + workflow("conditional") + .sequence("process", category) + .tasks( + doTasks( + 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?"); + + 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() { + + 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 astrologyAgent = AgentsUtils.newAstrologyAgent(); + + Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, 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 5b4c084c4ea40f75609ad1d0310ad943443accaf Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Thu, 25 Sep 2025 18:29:52 -0700 Subject: [PATCH 5/6] fix style Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/LC4JEquivalenceIT.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 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 56fe25294..90ee0f6a7 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,18 +180,22 @@ public void parallelWorkflow() { }))) .build(); - Workflow wf = workflow("forkFlow") - .tasks(d -> d - .parallel("fanout", foodExpert, movieExpert) - .callFn(fn((Map> m) -> { - var movies = m.getOrDefault("movies", List.of()); - var meals = m.getOrDefault("meals", List.of()); - return java.util.stream.IntStream - .range(0, Math.min(movies.size(), meals.size())) - .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) - .toList(); - })) - ).build(); + Workflow wf = + workflow("forkFlow") + .tasks( + d -> + d.parallel("fanout", foodExpert, movieExpert) + .callFn( + fn( + (Map> m) -> { + var movies = m.getOrDefault("movies", List.of()); + var meals = m.getOrDefault("meals", List.of()); + return java.util.stream.IntStream.range( + 0, Math.min(movies.size(), meals.size())) + .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) + .toList(); + }))) + .build(); List items = wf.getDo(); assertThat(items).hasSize(1); From d314a0470966adc7e8e55cc811e22d769706c125 Mon Sep 17 00:00:00 2001 From: Dmitrii Tikhomirov Date: Fri, 26 Sep 2025 11:51:50 -0700 Subject: [PATCH 6/6] one more fix Signed-off-by: Dmitrii Tikhomirov --- .../fluent/agentic/langchain4j/Agents.java | 1 - .../agentic/langchain4j/WorkflowAgentsIT.java | 1 - .../agentic/AgenticWorkflowHelperIT.java | 118 +++---- .../fluent/agentic/Agents.java | 1 - .../fluent/agentic/LC4JEquivalenceIT.java | 323 ------------------ 5 files changed, 55 insertions(+), 389 deletions(-) delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java index 730ef65f0..791792c3c 100644 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java +++ b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java @@ -59,5 +59,4 @@ public interface StyleEditor { @Agent("Edit a story to better fit a given style") String editStory(@V("story") String story, @V("style") String style); } - } diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java index d0d68f309..52e4d86f2 100644 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java +++ b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java @@ -77,5 +77,4 @@ void sequential_agents_tests() { verify(audienceEditor).editStory(any(), eq("young adults")); verify(styleEditor).editStory(any(), eq("fantasy")); } - } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java index a8e70613f..3ecc0fe79 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java @@ -28,27 +28,20 @@ import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newSummaryStory; 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 org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import dev.langchain4j.agentic.scope.AgenticScope; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.CallTaskJava; import io.serverlessworkflow.impl.WorkflowApplication; - import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.IntStream; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -61,9 +54,9 @@ public void sequentialWorkflow() { var audienceEditor = newAudienceEditor(); var styleEditor = newStyleEditor(); - NovelCreator novelCreator = AgenticWorkflow.of(NovelCreator.class) - .flow(workflow("seqFlow") - .sequence(creativeWriter, audienceEditor, styleEditor)) + NovelCreator novelCreator = + AgenticWorkflow.of(NovelCreator.class) + .flow(workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor)) .build(); String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); @@ -76,19 +69,20 @@ public void parallelWorkflow() { var foodExpert = newFoodExpert(); var movieExpert = newMovieExpert(); - Function> planEvening = input -> { - List movies = (List) input.readState("movies"); - List meals = (List) input.readState("meals"); + Function> planEvening = + input -> { + List movies = (List) input.readState("movies"); + List meals = (List) input.readState("meals"); - int max = Math.min(movies.size(), meals.size()); - return IntStream.range(0, max) + int max = Math.min(movies.size(), meals.size()); + return IntStream.range(0, max) .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) .toList(); - }; + }; - EveningPlannerAgent eveningPlannerAgent = AgenticWorkflow.of(EveningPlannerAgent.class) - .flow(workflow("parallelFlow") - .parallel(foodExpert, movieExpert).outputAs(planEvening)) + EveningPlannerAgent eveningPlannerAgent = + AgenticWorkflow.of(EveningPlannerAgent.class) + .flow(workflow("parallelFlow").parallel(foodExpert, movieExpert).outputAs(planEvening)) .build(); List result = eveningPlannerAgent.plan("romantic"); assertEquals(3, result.size()); @@ -103,11 +97,10 @@ public void loopTest() { Predicate until = s -> s.readState("score", 0.0) >= 0.8; - StyledWriter styledWriter = AgenticWorkflow.of(StyledWriter.class) - .flow(workflow("loopFlow") - .agent(creativeWriter) - .loop(until, styleScorer, styleEditor)) - .build(); + StyledWriter styledWriter = + AgenticWorkflow.of(StyledWriter.class) + .flow(workflow("loopFlow").agent(creativeWriter).loop(until, styleScorer, styleEditor)) + .build(); String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy"); assertNotNull(story); @@ -122,13 +115,13 @@ public void loopWorkflowWithMaxIterations() { var summaryStory = newSummaryStory(); NovelCreator novelCreator = - AgenticWorkflow.of(NovelCreator.class) - .flow( - workflow("seqFlow") - .agent(creativeWriter) - .sequence(audienceEditor, styleEditor) - .agent(summaryStory)) - .build(); + AgenticWorkflow.of(NovelCreator.class) + .flow( + workflow("seqFlow") + .agent(creativeWriter) + .sequence(audienceEditor, styleEditor) + .agent(summaryStory)) + .build(); String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); assertNotNull(story); @@ -140,25 +133,25 @@ public void humanInTheLoop() { var astrologyAgent = newAstrologyAgent(); var askSign = - new Function() { - @Override - public AgenticScope apply(AgenticScope holder) { - System.out.println("What's your star sign?"); - // var sign = System.console().readLine(); - holder.writeState("sign", "piscis"); - return holder; - } - }; + new Function() { + @Override + public AgenticScope apply(AgenticScope holder) { + System.out.println("What's your star sign?"); + // var sign = System.console().readLine(); + holder.writeState("sign", "piscis"); + return holder; + } + }; String result = - AgenticWorkflow.of(HoroscopeAgent.class) - .flow( - workflow("humanInTheLoop") - .inputFrom(askSign) - // .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too - .agent(astrologyAgent)) - .build() - .invoke("My name is Mario. What is my horoscope?"); + AgenticWorkflow.of(HoroscopeAgent.class) + .flow( + workflow("humanInTheLoop") + .inputFrom(askSign) + // .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too + .agent(astrologyAgent)) + .build() + .invoke("My name is Mario. What is my horoscope?"); assertNotNull(result); } @@ -171,9 +164,9 @@ public void errorHandling() { var styleEditor = newStyleEditor(); Workflow wf = - workflow("seqFlow") - .sequence("process", creativeWriter, audienceEditor, styleEditor) - .build(); + workflow("seqFlow") + .sequence("process", creativeWriter, audienceEditor, styleEditor) + .build(); List items = wf.getDo(); assertThat(items).hasSize(3); @@ -184,9 +177,9 @@ public void errorHandling() { items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); Map input = - Map.of( - "style", "fantasy", - "audience", "young adults"); + Map.of( + "style", "fantasy", + "audience", "young adults"); Map result; try (WorkflowApplication app = WorkflowApplication.builder().build()) { @@ -209,14 +202,14 @@ public void conditionalWorkflow() { var legalExpert = newLegalExpert(); Workflow wf = - workflow("conditional") - .sequence("process", category) - .tasks( - doTasks( - conditional(RequestCategory.MEDICAL::equals, medicalExpert), - conditional(RequestCategory.TECHNICAL::equals, technicalExpert), - conditional(RequestCategory.LEGAL::equals, legalExpert))) - .build(); + workflow("conditional") + .sequence("process", category) + .tasks( + doTasks( + conditional(RequestCategory.MEDICAL::equals, medicalExpert), + conditional(RequestCategory.TECHNICAL::equals, technicalExpert), + conditional(RequestCategory.LEGAL::equals, legalExpert))) + .build(); Map input = Map.of("question", "What is the best treatment for a common cold?"); @@ -229,5 +222,4 @@ public void conditionalWorkflow() { assertThat(result).containsKey("response"); } - } 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 902913d22..f91dae00f 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 @@ -406,5 +406,4 @@ interface EveningPlannerAgent { @Agent List plan(@V("mood") String mood); } - } 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 deleted file mode 100644 index 90ee0f6a7..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ /dev/null @@ -1,323 +0,0 @@ -/* - * 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 io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.loop; -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.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.ArrayList; -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; - -public class LC4JEquivalenceIT { - - @Test - @DisplayName("Sequential agents via DSL.sequence(...)") - public void sequentialWorkflow() { - var creativeWriter = AgentsUtils.newCreativeWriter(); - var audienceEditor = AgentsUtils.newAudienceEditor(); - var styleEditor = AgentsUtils.newStyleEditor(); - - Workflow wf = workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor).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(...)") - public void loopWorkflow() { - var creativeWriter = AgentsUtils.newCreativeWriter(); - var styleScorer = AgentsUtils.newStyleScorer(); - var styleEditor = AgentsUtils.newStyleEditor(); - - Workflow wf = - AgentWorkflowBuilder.workflow("retryFlow") - .agent(creativeWriter) - .loop( - "reviewLoop", - c -> c.readState("score", 0).doubleValue() >= 0.8, - styleScorer, - styleEditor) - .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("Looping agents via DSL.loop(...)") - public void loopWorkflowWithMaxIterations() { - var scorer = AgentsUtils.newStyleScorer(); - var editor = AgentsUtils.newStyleEditor(); - - Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; - - Workflow wf = workflow("retryFlow").tasks(loop(5, until, 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"); - } - - public record EveningPlan(String movie, String meal) {} - - @Test - @DisplayName("Parallel agents via DSL.parallel(...)") - public void parallelWorkflow() { - var foodExpert = AgentsUtils.newFoodExpert(); - var movieExpert = AgentsUtils.newMovieExpert(); - - workflow("forkFlow") - .tasks( - d -> - d.parallel(foodExpert, movieExpert) - .callFn( - fn( - f -> { - Map> asMap = (Map>) f; - List result = new ArrayList<>(); - int max = - asMap.values().stream() - .map(List::size) - .min(Integer::compareTo) - .orElse(0); - for (int i = 0; i < max; i++) { - result.add( - new EveningPlan( - asMap.get("movies").get(i), asMap.get("meals").get(i))); - } - return result; - }))) - .build(); - - Workflow wf = - workflow("forkFlow") - .tasks( - d -> - d.parallel("fanout", foodExpert, movieExpert) - .callFn( - fn( - (Map> m) -> { - var movies = m.getOrDefault("movies", List.of()); - var meals = m.getOrDefault("meals", List.of()); - return java.util.stream.IntStream.range( - 0, Math.min(movies.size(), meals.size())) - .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) - .toList(); - }))) - .build(); - - List items = wf.getDo(); - 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"); - - 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("Error handling with agents") - public void errorHandling() { - var creativeWriter = AgentsUtils.newCreativeWriter(); - var audienceEditor = AgentsUtils.newAudienceEditor(); - var styleEditor = AgentsUtils.newStyleEditor(); - - Workflow wf = - workflow("seqFlow") - .sequence("process", creativeWriter, audienceEditor, styleEditor) - .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"); - } - - @SuppressWarnings("unchecked") - @Test - @DisplayName("Conditional agents via choice(...)") - public void conditionalWorkflow() { - - var category = AgentsUtils.newCategoryRouter(); - var medicalExpert = AgentsUtils.newMedicalExpert(); - var technicalExpert = AgentsUtils.newTechnicalExpert(); - var legalExpert = AgentsUtils.newLegalExpert(); - - Workflow wf = - workflow("conditional") - .sequence("process", category) - .tasks( - doTasks( - 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?"); - - 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() { - - 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 astrologyAgent = AgentsUtils.newAstrologyAgent(); - - Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, 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"); - } -}