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..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 @@ -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( @@ -187,71 +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); } - - 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..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 @@ -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 { @@ -96,123 +77,4 @@ void sequential_agents_tests() { verify(audienceEditor).editStory(any(), eq("young adults")); 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/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/AgenticWorkflowHelperIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java new file mode 100644 index 000000000..3ecc0fe79 --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java @@ -0,0 +1,225 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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..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 @@ -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,12 @@ 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 59dd8a0d4..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java +++ /dev/null @@ -1,279 +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 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("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( - "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").loop(until, scorer, 5, 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 a9c2e469c..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 @@ -2,6 +2,8 @@ # Sequential workflow ### Common part: +
Click to expand + ```java public interface AudienceEditor { @@ -65,33 +67,49 @@ 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
+
+
+
+
+AgentsUtils.NovelCreator novelCreator = AgenticWorkflow.of(NovelCreator.class)
+            .flow(workflow("seqFlow")
+            .sequence(creativeWriter, audienceEditor, styleEditor))
+            .build();
+ 
+ 
+String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
 
-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,8 +154,23 @@ StyleScorer styleScorer = AgenticServices ``` -### LangChain4j -```java +
+ + + + + + + + + + + +
LangChain4jServerless Workflow
+
+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)
@@ -145,29 +178,38 @@ StyledWriter styledWriter = AgenticServices
         .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();
 
+
+
+
+
+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();
+ 
+ 
+ 
+ 
+ 
 
-Map input =
-        Map.of(
-                "story", "dragons and wizards",
-                "style", "comedy");
+String story = styledWriter.writeStoryWithStyle("dragons and wizards", "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: +
Click to expand + ```java public interface FoodExpert { @@ -206,75 +248,76 @@ MovieExpert movieExpert = AgenticServices .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(); +
+ + + + + + + + + + + + +
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();
 
 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
 
-```
+
+
+
+
+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();
+ 
+ 
+ 
+ 
+ 
+List result = eveningPlannerAgent.plan("romantic");
+
+
+
-### 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(); - } -} +
Click to expand +```java public interface AstrologyAgent { @SystemMessage(""" You are an astrologist that generates horoscopes based on the user's name and zodiac sign. @@ -290,21 +333,34 @@ 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(); ``` +
+ + + + + + + + + + + +
LangChain4jServerless Workflow
+
+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();
 
-### LangChain4j
-```java
 SupervisorAgent horoscopeAgent = AgenticServices
         .supervisorBuilder()
         .chatModel(PLANNER_MODEL)
@@ -312,18 +368,39 @@ SupervisorAgent horoscopeAgent = AgenticServices
         .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
+
+
+
+
+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 = AgenticWorkflow.of(HoroscopeAgent.class)
+  .flow(workflow("humanInTheLoop")
+  .inputFrom(askSign)
+  .agent(astrologyAgent))
+  .build()
+  .invoke("My name is Mario. What is my horoscope?");
+
+
+
\ No newline at end of file