diff --git a/fluent/agentic/pom.xml b/fluent/agentic/pom.xml index 2ebce363..02d1847d 100644 --- a/fluent/agentic/pom.xml +++ b/fluent/agentic/pom.xml @@ -60,6 +60,12 @@ test 1.2.0-SNAPSHOT + + io.serverlessworkflow + serverlessworkflow-experimental-lambda + ${project.version} + test + \ No newline at end of file diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 8248c28c..8ba9d8c7 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.agentic; import dev.langchain4j.agentic.Agent; +import dev.langchain4j.agentic.internal.AgentInstance; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; @@ -34,4 +35,136 @@ interface MovieExpert { @Agent List findMovie(@V("mood") String mood); } -} + + interface SettingAgent extends AgentInstance { + + @UserMessage( + """ + Create a vivid {{style}} setting. It should include the time period, the state of technology, + key locations, and a brief description of the world’s political or social situation. + Make it imaginative, atmospheric, and suitable for a {{style}} novel. + """) + @Agent( + "Generates an imaginative setting including timeline, technology level, and world structure") + String invoke(@V("style") String style); + } + + interface HeroAgent extends AgentInstance { + + @UserMessage( + """ + Invent a compelling protagonist for a {{style}} story. Describe their background, personality, + motivations, and any unique skills or traits. + """) + @Agent("Creates a unique and relatable protagonist with rich backstory and motivations.") + String invoke(@V("style") String style); + } + + interface ConflictAgent extends AgentInstance { + + @UserMessage( + """ + Generate a central conflict or threat for a {{style}} plot. It can be external or + internal (e.g. moral dilemma, personal transformation). + Make it high-stakes and thematically rich. + """) + @Agent("Proposes a central conflict or dramatic tension to drive a compelling narrative.") + String invoke(@V("style") String style); + } + + interface FactAgent extends AgentInstance { + + @UserMessage( + """ + Generate a unique sci-fi fact about an alien civilization's {{goal}} environment or evolutionary history. Make it imaginative and specific. + """) + @Agent("Generates a core fact that defines the foundation of an civilization.") + String invoke(@V("fact") String fact); + } + + interface CultureAgent extends AgentInstance { + + @UserMessage( + """ + Given the following sci-fi fact about an civilization, describe 3–5 unique cultural traits, traditions, or societal structures that naturally emerge from this environment. + Fact: + {{fact}} + """) + @Agent("Derives cultural traits from the environmental/evolutionary fact.") + List invoke(@V("fact") String fact); + } + + interface TechnologyAgent extends AgentInstance { + + @UserMessage( + """ + Given the following sci-fi fact about an alien civilization, describe 3–5 technologies or engineering solutions they might have developed. Focus on tools, transportation, communication, and survival systems. + Fact: + {{fact}} + """) + @Agent("Derives plausible technological inventions from the fact.") + List invoke(@V("fact") String fact); + } + + interface StorySeedAgent extends AgentInstance { + + @UserMessage( + """ + You are a science fiction writer. Given the following title, come up with a short story premise. Describe the world, the central concept, and the thematic direction (e.g., dystopia, exploration, AI ethics). + Title: {{title}} + """) + @Agent("Generates a high-level sci-fi premise based on a title.") + String invoke(@V("title") String title); + } + + interface PlotAgent extends AgentInstance { + + @UserMessage( + """ + Using the following premise, outline a three-act structure for a science fiction short story. Include a brief description of the main character, the inciting incident, the rising conflict, and the resolution. + Premise: + {{premise}} + """) + @Agent("Transforms a premise into a structured sci-fi plot.") + String invoke(@V("premise") String premise); + } + + interface SceneAgent extends AgentInstance { + + @UserMessage( + """ + Write the opening scene of a science fiction short story based on the following plot outline. Introduce the main character and immerse the reader in the setting. Use vivid, cinematic language. + Plot: + {{plot}} + """) + @Agent("Generates the opening scene of the story from a plot outline.") + String invoke(@V("plot") String plot); + } + + interface MeetingInvitationDraft extends AgentInstance { + + @UserMessage( + """ + You are a professional meeting invitation writer. Draft a concise and clear meeting invitation email based on the following details: + Subject: {{subject}} + Date: {{date}} + Time: {{time}} + Location: {{location}} + Agenda: {{agenda}} + """) + @Agent("Drafts a professional meeting invitation email.") + String invoke(@V("subject") String subject, @V("date") String date, @V("time") String time, @V("location") String location, @V("agenda") String agenda); + } + + interface MeetingInvitationStyle extends AgentInstance { + + @UserMessage( + """ + You are a professional meeting invitation writer. Rewrite the following meeting invitation email to better fit the {{style}} style: + Original Invitation: {{invitation}} + """) + @Agent("Edits a meeting invitation email to better fit a given style.") + String invoke(@V("invitation") String invitation, @V("style") String style); + } + + } diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java new file mode 100644 index 00000000..0071d0bd --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java @@ -0,0 +1,275 @@ +/* + * 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.Agents.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.workflow.HumanInTheLoop; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.WorkflowApplication; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class WorkflowTests { + + @Test + public void testAgent() throws ExecutionException, InterruptedException { + final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); + + when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); + when(storySeedAgent.outputName()).thenReturn("premise"); + + Workflow workflow = + AgentWorkflowBuilder.workflow("storyFlow") + .tasks(d -> d.agent("story", storySeedAgent)) + .build(); + + Map topic = new HashMap<>(); + topic.put("title", "A Great Story"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = + app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + + assertEquals("storySeedAgent", result); + } + } + + @Test + public void testAgents() throws ExecutionException, InterruptedException { + final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); + final PlotAgent plotAgent = mock(PlotAgent.class); + final SceneAgent sceneAgent = mock(SceneAgent.class); + + when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); + when(storySeedAgent.outputName()).thenReturn("premise"); + + when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent"); + when(plotAgent.outputName()).thenReturn("plot"); + + when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent"); + when(sceneAgent.outputName()).thenReturn("story"); + + Workflow workflow = + AgentWorkflowBuilder.workflow("storyFlow") + .tasks( + d -> + d.agent("story", storySeedAgent) + .agent("plot", plotAgent) + .agent("scene", sceneAgent)) + .build(); + + Map topic = new HashMap<>(); + topic.put("title", "A Great Story"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = + app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + + assertEquals("sceneAgent", result); + } + } + + @Test + public void testSequence() throws ExecutionException, InterruptedException { + final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); + final PlotAgent plotAgent = mock(PlotAgent.class); + final SceneAgent sceneAgent = mock(SceneAgent.class); + + when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); + when(storySeedAgent.outputName()).thenReturn("premise"); + + when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent"); + when(plotAgent.outputName()).thenReturn("plot"); + + when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent"); + when(sceneAgent.outputName()).thenReturn("story"); + + Workflow workflow = + AgentWorkflowBuilder.workflow("storyFlow") + .tasks(d -> d.sequence("story", storySeedAgent, plotAgent, sceneAgent)) + .build(); + + Map topic = new HashMap<>(); + topic.put("title", "A Great Story"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = + app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + + assertEquals("sceneAgent", result); + } + } + + @Test + public void testParallel() throws ExecutionException, InterruptedException { + + final SettingAgent setting = mock(SettingAgent.class); + final HeroAgent hero = mock(HeroAgent.class); + final ConflictAgent conflict = mock(ConflictAgent.class); + + when(setting.invoke(eq("sci-fi"))).thenReturn("Fake conflict response"); + when(setting.outputName()).thenReturn("setting"); + + when(hero.invoke(eq("sci-fi"))).thenReturn("Fake hero response"); + when(hero.outputName()).thenReturn("hero"); + + when(conflict.invoke(eq("sci-fi"))).thenReturn("Fake setting response"); + when(conflict.outputName()).thenReturn("conflict"); + + Workflow workflow = + AgentWorkflowBuilder.workflow("parallelFlow") + .tasks(d -> d.parallel("story", setting, hero, conflict)) + .build(); + + Map topic = new HashMap<>(); + topic.put("style", "sci-fi"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + Map result = + app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); + + assertEquals(3, result.size()); + assertTrue(result.containsKey("branch-0-story")); + assertTrue(result.containsKey("branch-1-story")); + assertTrue(result.containsKey("branch-2-story")); + + Set values = + result.values().stream().map(Object::toString).collect(Collectors.toSet()); + + assertTrue(values.contains("Fake conflict response")); + assertTrue(values.contains("Fake hero response")); + assertTrue(values.contains("Fake setting response")); + } + } + + @Test + // TODO: callFn must be replace with a .output() method once it's available + public void testSeqAndThenParallel() throws ExecutionException, InterruptedException { + final FactAgent factAgent = mock(FactAgent.class); + final CultureAgent cultureAgent = mock(CultureAgent.class); + final TechnologyAgent technologyAgent = mock(TechnologyAgent.class); + + List cultureTraits = + List.of("Alien Culture Trait 1", "Alien Culture Trait 2", "Alien Culture Trait 3"); + + List technologyTraits = + List.of("Alien Technology Trait 1", "Alien Technology Trait 2", "Alien Technology Trait 3"); + + when(factAgent.invoke(eq("alien"))).thenReturn("Some Fact about aliens"); + when(factAgent.outputName()).thenReturn("fact"); + + when(cultureAgent.invoke(eq("Some Fact about aliens"))).thenReturn(cultureTraits); + when(cultureAgent.outputName()).thenReturn("culture"); + + when(technologyAgent.invoke(eq("Some Fact about aliens"))).thenReturn(technologyTraits); + when(technologyAgent.outputName()).thenReturn("technology"); + Workflow workflow = + AgentWorkflowBuilder.workflow("alienCultureFlow") + .tasks( + d -> + d.sequence("fact", factAgent) + .callFn( + f -> + f.function( + (Function>) + fact -> { + Map result = new HashMap<>(); + result.put("fact", fact); + return result; + })) + .parallel("cultureAndTechnology", cultureAgent, technologyAgent)) + .build(); + + Map topic = new HashMap<>(); + topic.put("fact", "alien"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + Map result = + app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); + + assertEquals(2, result.size()); + assertTrue(result.containsKey("branch-0-cultureAndTechnology")); + assertTrue(result.containsKey("branch-1-cultureAndTechnology")); + + assertEquals(cultureTraits, result.get("branch-0-cultureAndTechnology")); + assertEquals(technologyTraits, result.get("branch-1-cultureAndTechnology")); + } + } + + @Test + public void humanInTheLoop() throws ExecutionException, InterruptedException { + final MeetingInvitationDraft meetingInvitationDraft = mock(MeetingInvitationDraft.class); + when(meetingInvitationDraft.invoke(eq("Meeting with John Doe"), + eq("2023-10-01"), eq("08:00AM"), + eq("London"), + eq("Discuss project updates"))) + .thenReturn("Drafted meeting invitation for John Doe"); + when(meetingInvitationDraft.outputName()).thenReturn("draft"); + + + final MeetingInvitationStyle meetingInvitationStyle = mock(MeetingInvitationStyle.class); + when(meetingInvitationStyle.invoke(eq("Drafted meeting invitation for John Doe"), eq("formal"))) + .thenReturn("Styled meeting invitation for John Doe"); + when(meetingInvitationStyle.outputName()).thenReturn("styled"); + + AtomicReference request = new AtomicReference<>(); + + HumanInTheLoop humanInTheLoop = AgentServices.humanInTheLoopBuilder() + .description("What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)") + .inputName("style") + .outputName("style") + .requestWriter(q -> request.set("What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)")) + .responseReader(() -> "formal") + .build(); + + Workflow workflow = + AgentWorkflowBuilder.workflow("meetingInvitationFlow") + .tasks( + d -> + d.sequence("draft", meetingInvitationDraft, humanInTheLoop, meetingInvitationStyle) + ).build(); + Map initialValues = new HashMap<>(); + initialValues.put("title", "Meeting with John Doe"); + initialValues.put("date", "2023-10-01"); + initialValues.put("time", "08:00AM"); + initialValues.put("location", "London"); + initialValues.put("agenda", "Discuss project updates"); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + String result = + app.workflowDefinition(workflow).instance(initialValues).start().get().asText().orElseThrow(); + + assertEquals("Styled meeting invitation for John Doe", result); + } + + + } +}