diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java index f6505ef0..ccf77f37 100644 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java @@ -121,6 +121,13 @@ public static AgentTaskConfigurer loop(Predicate exitCondition, Ob return list -> list.loop(l -> l.subAgents(agents).exitCondition(exitCondition)); } + public static AgentTaskConfigurer loop( + Predicate exitCondition, int maxIterations, Object... agents) { + return list -> + list.loop( + l -> l.subAgents(agents).exitCondition(exitCondition).maxIterations(maxIterations)); + } + public static AgentTaskConfigurer parallel(Object... agents) { return list -> list.parallel(agents); } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 5c10764e..434867b7 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -15,8 +15,10 @@ */ package io.serverlessworkflow.fluent.agentic; +import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; +import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; @@ -234,4 +236,143 @@ String draftNew( @V("allowedDomains") List allowedDomains, @V("links") List links); } + + interface CreativeWriter { + + @UserMessage( + """ + You are a creative writer. + Generate a draft of a story no more than + 3 sentences long around the given topic. + Return only the story and nothing else. + The topic is {{topic}}. + """) + @Agent("Generates a story based on the given topic") + String generateStory(@V("topic") String topic); + } + + interface AudienceEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better align + with the target audience of {{audience}}. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given audience") + String editStory(@V("story") String story, @V("audience") String audience); + } + + interface StyleEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); + } + + interface StyleScorer { + + @UserMessage( + """ + You are a critical reviewer. + Give a review score between 0.0 and 1.0 for the following + story based on how well it aligns with the style '{{style}}'. + Return only the score and nothing else. + + The story is: "{{story}}" + """) + @Agent("Scores a story based on how well it aligns with a given style") + double scoreStyle(@V("story") String story, @V("style") String style); + } + + interface FoodExpert { + + @UserMessage( + """ + You are a great evening planner. + Propose a list of 3 meals matching the given mood. + The mood is {{mood}}. + For each meal, just give the name of the meal. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMeal(@V("mood") String mood); + } + + interface AstrologyAgent { + @SystemMessage( + """ + You are an astrologist that generates horoscopes based on the user's name and zodiac sign. + """) + @UserMessage( + """ + Generate the horoscope for {{name}} who is a {{sign}}. + """) + @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") + String horoscope(@V("name") String name, @V("sign") String sign); + } + + enum RequestCategory { + LEGAL, + MEDICAL, + TECHNICAL, + UNKNOWN + } + + interface CategoryRouter { + + @UserMessage( + """ + Analyze the following user request and categorize it as 'legal', 'medical' or 'technical'. + In case the request doesn't belong to any of those categories categorize it as 'unknown'. + Reply with only one of those words and nothing else. + The user request is: '{{request}}'. + """) + @Agent("Categorizes a user request") + RequestCategory classify(@V("request") String request); + } + + interface MedicalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a medical expert. + Analyze the following user request under a medical point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A medical expert") + String medicalRequest(String request); + } + + interface LegalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a legal expert. + Analyze the following user request under a legal point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A legal expert") + String legalRequest(String request); + } + + interface TechnicalExpert { + + @dev.langchain4j.service.UserMessage( + """ + You are a technical expert. + Analyze the following user request under a technical point of view and provide the best possible answer. + The user request is {{it}}. + """) + @Tool("A technical expert") + String technicalRequest(String request); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java index f44d2a22..42df6b4c 100644 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -31,4 +31,84 @@ public static Agents.MovieExpert newMovieExpert() { .chatModel(BASE_MODEL) .build()); } + + public static Agents.CreativeWriter newCreativeWriter() { + return spy( + AgenticServices.agentBuilder(Agents.CreativeWriter.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.AudienceEditor newAudienceEditor() { + return spy( + AgenticServices.agentBuilder(Agents.AudienceEditor.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.StyleEditor newStyleEditor() { + return spy( + AgenticServices.agentBuilder(Agents.StyleEditor.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.StyleScorer newStyleScorer() { + return spy( + AgenticServices.agentBuilder(Agents.StyleScorer.class) + .outputName("score") + .chatModel(BASE_MODEL) + .build()); + } + + public static Agents.FoodExpert newFoodExpert() { + return spy( + AgenticServices.agentBuilder(Agents.FoodExpert.class) + .chatModel(BASE_MODEL) + .outputName("meals") + .build()); + } + + public static Agents.AstrologyAgent newAstrologyAgent() { + return spy( + AgenticServices.agentBuilder(Agents.AstrologyAgent.class) + .chatModel(BASE_MODEL) + .outputName("horoscope") + .build()); + } + + public static Agents.CategoryRouter newCategoryRouter() { + return spy( + AgenticServices.agentBuilder(Agents.CategoryRouter.class) + .chatModel(BASE_MODEL) + .outputName("category") + .build()); + } + + public static Agents.MedicalExpert newMedicalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.MedicalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } + + public static Agents.TechnicalExpert newTechnicalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.TechnicalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } + + public static Agents.LegalExpert newLegalExpert() { + return spy( + AgenticServices.agentBuilder(Agents.LegalExpert.class) + .chatModel(BASE_MODEL) + .outputName("response") + .build()); + } } diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java new file mode 100644 index 00000000..492b373c --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/LC4JEquivalenceIT.java @@ -0,0 +1,278 @@ +/* + * 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 scorer = AgentsUtils.newStyleScorer(); + var editor = AgentsUtils.newStyleEditor(); + + Workflow wf = + AgentWorkflowBuilder.workflow("retryFlow") + .loop("reviewLoop", c -> c.readState("score", 0).doubleValue() >= 0.8, scorer, editor) + .build(); + + List items = wf.getDo(); + assertThat(items).hasSize(1); + + var fn = (ForTaskFunction) items.get(0).getTask().getForTask(); + assertThat(fn.getDo()).isNotNull(); + assertThat(fn.getDo()).hasSize(2); + fn.getDo() + .forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); + + Map input = + Map.of( + "story", "dragons and wizards", + "style", "comedy"); + + Map result; + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + + assertThat(result).containsKey("story"); + } + + @Test + @DisplayName("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 new file mode 100644 index 00000000..a9c2e469 --- /dev/null +++ b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md @@ -0,0 +1,329 @@ +# Implementation of Scenarios from [LangChain4j Agents Tutorials](https://docs.langchain4j.dev/tutorials/agents/) for CNCF Workflow Java DSL + +# Sequential workflow +### Common part: +```java +public interface AudienceEditor { + + @UserMessage(""" + You are a professional editor. + Analyze and rewrite the following story to better align + with the target audience of {{audience}}. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given audience") + String editStory(@V("story") String story, @V("audience") String audience); +} + +interface CreativeWriter { + + @UserMessage(""" + You are a creative writer. + Generate a draft of a story no more than + 3 sentences long around the given topic. + Return only the story and nothing else. + The topic is {{topic}}. + """) + @Agent("Generates a story based on the given topic") + String generateStory(@V("topic") String topic); +} + +public interface StyleEditor { + + @UserMessage(""" + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); +} + +CreativeWriter creativeWriter = AgenticServices + .agentBuilder(CreativeWriter.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +AudienceEditor audienceEditor = AgenticServices + .agentBuilder(AudienceEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +StyleEditor styleEditor = AgenticServices + .agentBuilder(StyleEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +Map input = Map.of( + "topic", "dragons and wizards", + "style", "fantasy", + "audience", "young adults" +); +``` + +### LangChain4j +```java +UntypedAgent novelCreator = AgenticServices + .sequenceBuilder() + .subAgents(creativeWriter, audienceEditor, styleEditor) + .outputName("story") + .build(); + +String story = (String) novelCreator.invoke(input); +``` + +### Serverless Workflow + +```java +Workflow wf = workflow("seqFlow").sequence("process", creativeWriter, audienceEditor, styleEditor).build(); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + + +### Loop workflow +### Common part: +```java + interface StyleEditor { + + @UserMessage( + """ + You are a professional editor. + Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. + Return only the story and nothing else. + The story is "{{story}}". + """) + @Agent("Edits a story to better fit a given style") + String editStory(@V("story") String story, @V("style") String style); +} + +interface StyleScorer { + + @UserMessage( + """ + You are a critical reviewer. + Give a review score between 0.0 and 1.0 for the following + story based on how well it aligns with the style '{{style}}'. + Return only the score and nothing else. + + The story is: "{{story}}" + """) + @Agent("Scores a story based on how well it aligns with a given style") + double scoreStyle(@V("story") String story, @V("style") String style); +} + +StyleEditor styleEditor = AgenticServices + .agentBuilder(StyleEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build(); + +StyleScorer styleScorer = AgenticServices + .agentBuilder(StyleScorer.class) + .chatModel(BASE_MODEL) + .outputName("score") + .build(); + + +``` + +### LangChain4j +```java +StyledWriter styledWriter = AgenticServices + .sequenceBuilder(StyledWriter.class) + .subAgents(creativeWriter, styleReviewLoop) + .outputName("story") + .build(); + +String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy"); +``` + +### Serverless Workflow +```java +Predicate until = s -> s.readState("score", 0).doubleValue() >= 0.8; + +Workflow wf = workflow("retryFlow").loop(until, scorer, 5, editor).build(); + + +Map input = + Map.of( + "story", "dragons and wizards", + "style", "comedy"); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asText().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + +### Parallel workflow +### Common part: +```java +public interface FoodExpert { + + @UserMessage(""" + You are a great evening planner. + Propose a list of 3 meals matching the given mood. + The mood is {{mood}}. + For each meal, just give the name of the meal. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMeal(@V("mood") String mood); +} + +public interface MovieExpert { + + @UserMessage(""" + You are a great evening planner. + Propose a list of 3 movies matching the given mood. + The mood is {mood}. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMovie(@V("mood") String mood); +} + +FoodExpert foodExpert = AgenticServices + .agentBuilder(FoodExpert.class) + .chatModel(BASE_MODEL) + .outputName("meals") + .build(); + +MovieExpert movieExpert = AgenticServices + .agentBuilder(MovieExpert.class) + .chatModel(BASE_MODEL) + .outputName("movies") + .build(); +``` + +### LangChain4j +```java +EveningPlannerAgent eveningPlannerAgent = AgenticServices + .parallelBuilder(EveningPlannerAgent.class) + .subAgents(foodExpert, movieExpert) + .executor(Executors.newFixedThreadPool(2)) + .outputName("plans") + .output(agenticScope -> { + List movies = agenticScope.readState("movies", List.of()); + List meals = agenticScope.readState("meals", List.of()); + + List moviesAndMeals = new ArrayList<>(); + for (int i = 0; i < movies.size(); i++) { + if (i >= meals.size()) { + break; + } + moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i))); + } + return moviesAndMeals; + }) + .build(); + +List plans = eveningPlannerAgent.plan("romantic"); +``` + +### Serverless Workflow +```java +Workflow wf = workflow("forkFlow").parallel("fanout", foodExpert, movieExpert).build(); + +Map input = Map.of("mood", "I am hungry and bored"); + +Map result; +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` + +### Error handling +### Common part: +```java + +``` + +### LangChain4j +```java + +``` + +### Serverless Workflow + +```java + +``` + +### Human-in-the-loop +### Common part: +```java +public record HumanInTheLoop(Consumer requestWriter, Supplier responseReader) { + + @Agent("An agent that asks the user for missing information") + public String askUser(String request) { + requestWriter.accept(request); + return responseReader.get(); + } +} + +public interface AstrologyAgent { + @SystemMessage(""" + You are an astrologist that generates horoscopes based on the user's name and zodiac sign. + """) + @UserMessage(""" + Generate the horoscope for {{name}} who is a {{sign}}. + """) + @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") + String horoscope(@V("name") String name, @V("sign") String sign); +} + +AstrologyAgent astrologyAgent = AgenticServices + .agentBuilder(AstrologyAgent.class) + .chatModel(BASE_MODEL) + .build(); + +HumanInTheLoop humanInTheLoop = AgenticServices + .humanInTheLoopBuilder() + .description("An agent that asks the zodiac sign of the user") + .outputName("sign") + .requestWriter(request -> { + System.out.println(request); + System.out.print("> "); + }) + .responseReader(() -> System.console().readLine()) + .build(); +``` + +### LangChain4j +```java +SupervisorAgent horoscopeAgent = AgenticServices + .supervisorBuilder() + .chatModel(PLANNER_MODEL) + .subAgents(astrologyAgent, humanInTheLoop) + .build(); + +horoscopeAgent.invoke("My name is Mario. What is my horoscope?") +``` + +### Serverless Workflow + +```java +Workflow wf = workflow("seqFlow").sequence("process", astrologyAgent, humanInTheLoop).build(); + +Map input = Map.of("request", "My name is Mario. What is my horoscope?"); + +try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); +} catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); +} +``` \ No newline at end of file