diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java index ef1f0a55..0d34d424 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java @@ -55,4 +55,9 @@ public Optional as(Class clazz) { return super.as(clazz); } } + + @Override + public Object asJavaObject() { + return agenticScope; + } } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java index 10476aa3..d14dca88 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java @@ -23,8 +23,8 @@ import io.serverlessworkflow.impl.WorkflowModelFactory; import io.serverlessworkflow.impl.expressions.agentic.langchain4j.AgenticScopeRegistryAssessor; import java.time.OffsetDateTime; +import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; class AgenticModelFactory implements WorkflowModelFactory { @@ -55,17 +55,22 @@ public WorkflowModel fromAny(WorkflowModel prev, Object obj) { // hood, the agent already updated it. if (prev instanceof AgenticModel agenticModel) { this.scopeRegistryAssessor.setAgenticScope(agenticModel.getAgenticScope()); + agenticModel.getAgenticScope().state().put(DEFAULT_AGENTIC_SCOPE_STATE_KEY, obj); } return newAgenticModel(obj); } @Override public WorkflowModel combine(Map workflowVariables) { - Map combinedState = - workflowVariables.entrySet().stream() - .map(e -> Map.entry(e.getKey(), e.getValue().asJavaObject())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - return newAgenticModel(combinedState); + Map map = new HashMap<>(); + for (Map.Entry e : workflowVariables.entrySet()) { + if (e.getValue() instanceof AgenticModel agenticModel) { + map.putAll(agenticModel.getAgenticScope().state()); + } else { + map.put(e.getKey(), e.getValue().asJavaObject()); + } + } + return newAgenticModel(map); } @Override 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 d29a01af..f2b6359d 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 @@ -18,7 +18,6 @@ import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.scope.AgenticScopeAccess; -import dev.langchain4j.agentic.scope.ResultWithAgenticScope; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; @@ -212,8 +211,7 @@ public interface StyleReviewLoop { public interface StyledWriter extends AgenticScopeAccess { @Agent - ResultWithAgenticScope writeStoryWithStyle( - @V("topic") String topic, @V("style") String style); + String writeStoryWithStyle(@V("topic") String topic, @V("style") String style); } public interface FoodExpert { @@ -250,4 +248,10 @@ 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 52e4d86f..f5c779d5 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,19 +15,37 @@ */ 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; import static org.mockito.Mockito.verify; -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.AgenticServices; +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 { @@ -38,21 +56,21 @@ void sequential_agents_tests() { CreativeWriter creativeWriter = spy( - AgenticServices.agentBuilder(CreativeWriter.class) + dev.langchain4j.agentic.AgenticServices.agentBuilder(CreativeWriter.class) .chatModel(BASE_MODEL) .outputName("story") .build()); AudienceEditor audienceEditor = spy( - AgenticServices.agentBuilder(AudienceEditor.class) + dev.langchain4j.agentic.AgenticServices.agentBuilder(AudienceEditor.class) .chatModel(BASE_MODEL) .outputName("story") .build()); StyleEditor styleEditor = spy( - AgenticServices.agentBuilder(StyleEditor.class) + dev.langchain4j.agentic.AgenticServices.agentBuilder(StyleEditor.class) .chatModel(BASE_MODEL) .outputName("story") .build()); @@ -77,4 +95,123 @@ 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 = + io.serverlessworkflow.fluent.agentic.AgenticServices.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 = + io.serverlessworkflow.fluent.agentic.AgenticServices.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 = + io.serverlessworkflow.fluent.agentic.AgenticServices.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 = + AgenticServices.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 = + AgenticServices.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 = + AgenticServices.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/AgenticServices.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticServices.java new file mode 100644 index 00000000..e687dad8 --- /dev/null +++ b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticServices.java @@ -0,0 +1,134 @@ +/* + * 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 dev.langchain4j.agentic.Agent; +import dev.langchain4j.agentic.scope.AgenticScope; +import dev.langchain4j.service.V; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowModel; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class AgenticServices { + + private final Class agent; + + private AgentWorkflowBuilder builder; + + private AgenticServices(Class agent) { + this.agent = agent; + } + + public static AgenticServices of(Class agent) { + return new AgenticServices<>(agent); + } + + public AgenticServices flow(AgentWorkflowBuilder builder) { + this.builder = builder; + return this; + } + + public T build() { + Objects.requireNonNull( + builder, "AgenticServices.flow(AgentWorkflowBuilder) must be called before build()"); + Workflow workflow = builder.build(); + return AgenticServiceBuilder.create(agent, new AgentInvocationHandler(workflow)); + } + + private static class AgenticServiceBuilder { + + @SuppressWarnings("unchecked") + public static T create(Class runner, InvocationHandler h) { + if (!runner.isInterface()) { + throw new IllegalArgumentException(runner + " must be an interface to create a Proxy"); + } + + ClassLoader cl = runner.getClassLoader(); + Class[] ifaces = new Class[] {runner}; + return (T) Proxy.newProxyInstance(cl, ifaces, h); + } + } + + private class AgentInvocationHandler implements InvocationHandler { + + private final Workflow workflow; + + public AgentInvocationHandler(Workflow workflow) { + this.workflow = workflow; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if (method.getDeclaringClass() == Object.class) { + return switch (method.getName()) { + case "toString" -> "AgentProxy(" + workflow.getDocument().getName() + ")"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> throw new IllegalStateException("Unexpected Object method: " + method); + }; + } + + Agent agent = method.getAnnotation(Agent.class); + if (agent == null) { + throw new IllegalStateException( + "Method " + method.getName() + " is not annotated with @Agent"); + } + + Annotation[][] annotations = method.getParameterAnnotations(); + Map input = new HashMap<>(); + for (int i = 0; i < annotations.length; i++) { + boolean found = false; + for (Annotation a : annotations[i]) { + if (a instanceof V) { + String key = ((V) a).value(); + Object value = args[i]; + input.put(key, value); + found = true; + break; + } + } + if (!found) { + throw new IllegalStateException( + "Parameter " + + (i + 1) + + " of method " + + method.getName() + + " is not annotated with @V"); + } + } + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + WorkflowModel result = app.workflowDefinition(workflow).instance(input).start().get(); + if (result.asJavaObject() instanceof AgenticScope scope) { + Object out = scope.state().get("input"); + if (out != null) { + return out; + } + } + return result.asJavaObject(); + } catch (Exception e) { + throw new RuntimeException("Workflow execution failed", e); + } + } + } +} 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 434867b7..d9c4bf47 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 @@ -293,6 +293,16 @@ interface StyleScorer { double scoreStyle(@V("story") String story, @V("style") String style); } + interface SummaryStory { + + @UserMessage( + """ + Return only the summary text and nothing else. + """) + @Agent("Edits a story to better fit a given audience") + String summaryStory(@V("story") String story); + } + interface FoodExpert { @UserMessage( @@ -320,6 +330,12 @@ interface AstrologyAgent { String horoscope(@V("name") String name, @V("sign") String sign); } + interface HoroscopeAgent { + + @Agent + String invoke(@V("name") String name); + } + enum RequestCategory { LEGAL, MEDICAL, 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 9fcd5773..81823e36 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 @@ -18,7 +18,9 @@ import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; import static org.mockito.Mockito.spy; +import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.service.V; public final class AgentsUtils { @@ -65,6 +67,14 @@ public static Agents.StyleEditor newStyleEditor() { .build()); } + public static Agents.SummaryStory newSummaryStory() { + return spy( + AgenticServices.agentBuilder(Agents.SummaryStory.class) + .outputName("story") + .chatModel(BASE_MODEL) + .build()); + } + public static Agents.StyleScorer newStyleScorer() { return spy( AgenticServices.agentBuilder(Agents.StyleScorer.class) @@ -120,4 +130,11 @@ public static Agents.LegalExpert newLegalExpert() { .outputName("response") .build()); } + + public interface NovelCreator { + + @Agent + String createNovel( + @V("topic") String topic, @V("audience") String audience, @V("style") String style); + } } 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 492b373c..59dd8a0d 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 @@ -77,12 +77,13 @@ public void sequentialWorkflow() { @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();