diff --git a/experimental/lambda/src/test/java/io/serverless/workflow/impl/FluentDSLCallTest.java b/experimental/lambda/src/test/java/io/serverless/workflow/impl/FluentDSLCallTest.java index 9706be4f..dcad95d8 100644 --- a/experimental/lambda/src/test/java/io/serverless/workflow/impl/FluentDSLCallTest.java +++ b/experimental/lambda/src/test/java/io/serverless/workflow/impl/FluentDSLCallTest.java @@ -34,7 +34,7 @@ void testJavaFunction() throws InterruptedException, ExecutionException { try (WorkflowApplication app = WorkflowApplication.builder().build()) { final Workflow workflow = FuncWorkflowBuilder.workflow("testJavaCall") - .tasks(tasks -> tasks.callFn(f -> f.fn(JavaFunctions::getName))) + .tasks(tasks -> tasks.callFn(f -> f.function(JavaFunctions::getName))) .build(); assertThat( app.workflowDefinition(workflow) @@ -85,7 +85,7 @@ void testSwitch() throws InterruptedException, ExecutionException { switchOdd.items( item -> item.when(CallTest::isOdd).then(FlowDirectiveEnum.END))) - .callFn(callJava -> callJava.fn(CallTest::zero))) + .callFn(callJava -> callJava.function(CallTest::zero))) .build(); WorkflowDefinition definition = app.workflowDefinition(workflow); diff --git a/fluent/agentic/pom.xml b/fluent/agentic/pom.xml new file mode 100644 index 00000000..74869509 --- /dev/null +++ b/fluent/agentic/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-fluent + 8.0.0-SNAPSHOT + + + Serverless Workflow :: Fluent :: Agentic + serverlessworkflow-fluent-agentic + + + 17 + 17 + UTF-8 + + 1.2.0-beta8-SNAPSHOT + + + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + io.serverlessworkflow + serverlessworkflow-types + + + io.serverlessworkflow + serverlessworkflow-fluent-spec + + + io.serverlessworkflow + serverlessworkflow-fluent-func + + + dev.langchain4j + langchain4j-agentic + ${version.dev.langchain4j} + + + org.slf4j + slf4j-simple + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + + + dev.langchain4j + langchain4j-ollama + test + 1.2.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java new file mode 100644 index 00000000..eafd2a82 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java @@ -0,0 +1,43 @@ +/* + * 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 dev.langchain4j.agentic.internal.AgentExecutor.agentsToExecutors; + +import dev.langchain4j.agentic.Cognisphere; +import dev.langchain4j.agentic.internal.AgentExecutor; +import dev.langchain4j.agentic.internal.AgentInstance; +import io.serverlessworkflow.impl.expressions.LoopPredicateIndex; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public final class AgentAdapters { + private AgentAdapters() {} + + public static List toExecutors(Object... agents) { + return agentsToExecutors(Stream.of(agents).map(AgentInstance.class::cast).toList()); + } + + public static Function toFunction(AgentExecutor exec) { + return exec::invoke; + } + + public static LoopPredicateIndex toWhile(Predicate exit) { + return (model, item, idx) -> !exit.test((Cognisphere) model); + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java new file mode 100644 index 00000000..3d9b1cbc --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java @@ -0,0 +1,32 @@ +/* + * 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 io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; + +public class AgentDoTaskBuilder + extends BaseDoTaskBuilder + implements DelegatingAgentDoTaskFluent { + + AgentDoTaskBuilder() { + super(new AgentTaskItemListBuilder()); + } + + @Override + public AgentDoTaskBuilder self() { + return this; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java new file mode 100644 index 00000000..59055322 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java @@ -0,0 +1,46 @@ +/* + * 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 java.util.UUID; +import java.util.function.Consumer; + +public interface AgentDoTaskFluent> { + + SELF agent(String name, Object agent); + + default SELF agent(Object agent) { + return agent(UUID.randomUUID().toString(), agent); + } + + SELF sequence(String name, Object... agents); + + default SELF sequence(Object... agents) { + return sequence("seq-" + UUID.randomUUID(), agents); + } + + SELF loop(String name, Consumer builder); + + default SELF loop(Consumer builder) { + return loop("loop-" + UUID.randomUUID(), builder); + } + + SELF parallel(String name, Object... agents); + + default SELF parallel(Object... agents) { + return parallel("par-" + UUID.randomUUID(), agents); + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java new file mode 100644 index 00000000..6f1012fe --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -0,0 +1,84 @@ +/* + * 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.internal.AgentExecutor; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; +import java.util.List; +import java.util.function.Consumer; + +public class AgentTaskItemListBuilder extends BaseTaskItemListBuilder + implements AgentDoTaskFluent { + + private final FuncTaskItemListBuilder funcDelegate; + + AgentTaskItemListBuilder() { + super(); + this.funcDelegate = new FuncTaskItemListBuilder(super.mutableList()); + } + + @Override + protected AgentTaskItemListBuilder newItemListBuilder() { + return new AgentTaskItemListBuilder(); + } + + @Override + public AgentTaskItemListBuilder agent(String name, Object agent) { + AgentAdapters.toExecutors(agent) + .forEach( + exec -> + this.funcDelegate.callFn(name, fn -> fn.function(AgentAdapters.toFunction(exec)))); + return self(); + } + + @Override + public AgentTaskItemListBuilder sequence(String name, Object... agents) { + for (int i = 0; i < agents.length; i++) { + agent(name + "-" + i, agents[i]); + } + return self(); + } + + @Override + public AgentTaskItemListBuilder loop(String name, Consumer consumer) { + final LoopAgentsBuilder builder = new LoopAgentsBuilder(); + consumer.accept(builder); + this.addTaskItem(new TaskItem(name, new Task().withForTask(builder.build()))); + return self(); + } + + @Override + public AgentTaskItemListBuilder parallel(String name, Object... agents) { + this.funcDelegate.forkFn( + name, + fork -> { + List execs = AgentAdapters.toExecutors(agents); + for (int i = 0; i < execs.size(); i++) { + AgentExecutor ex = execs.get(i); + fork.branch("branch-" + i + "-" + name, AgentAdapters.toFunction(ex)); + } + }); + return self(); + } + + @Override + public AgentTaskItemListBuilder self() { + return this; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java new file mode 100644 index 00000000..cef01011 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java @@ -0,0 +1,51 @@ +/* + * 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 java.lang.constant.ConstantDescs.DEFAULT_NAME; + +import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; + +public final class AgenticWorkflowBuilder + extends BaseWorkflowBuilder< + AgenticWorkflowBuilder, AgentDoTaskBuilder, AgentTaskItemListBuilder> { + + AgenticWorkflowBuilder(final String name, final String namespace, final String version) { + super(name, namespace, version); + } + + public static AgenticWorkflowBuilder workflow() { + return new AgenticWorkflowBuilder(DEFAULT_NAME, DEFAULT_NAMESPACE, DEFAULT_VERSION); + } + + public static AgenticWorkflowBuilder workflow(String name) { + return new AgenticWorkflowBuilder(name, DEFAULT_NAMESPACE, DEFAULT_VERSION); + } + + public static AgenticWorkflowBuilder workflow(String name, String ns) { + return new AgenticWorkflowBuilder(name, ns, DEFAULT_VERSION); + } + + @Override + protected AgentDoTaskBuilder newDo() { + return new AgentDoTaskBuilder(); + } + + @Override + protected AgenticWorkflowBuilder self() { + return this; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java new file mode 100644 index 00000000..7f89ca0f --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -0,0 +1,57 @@ +/* + * 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 io.serverlessworkflow.fluent.func.DelegatingFuncDoTaskFluent; +import io.serverlessworkflow.fluent.spec.HasDelegate; +import java.util.function.Consumer; + +public interface DelegatingAgentDoTaskFluent> + extends AgentDoTaskFluent, DelegatingFuncDoTaskFluent, HasDelegate { + + @SuppressWarnings("unchecked") + private AgentDoTaskFluent d() { + return (AgentDoTaskFluent) this.delegate(); + } + + @SuppressWarnings("unchecked") + @Override + default SELF agent(String name, Object agent) { + d().agent(name, agent); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + @Override + default SELF sequence(String name, Object... agents) { + d().sequence(name, agents); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + @Override + default SELF loop(String name, Consumer consumer) { + d().loop(name, consumer); + return (SELF) this; + } + + @SuppressWarnings("unchecked") + @Override + default SELF parallel(String name, Object... agents) { + d().parallel(name, agents); + return (SELF) this; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java new file mode 100644 index 00000000..732b4c95 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java @@ -0,0 +1,72 @@ +/* + * 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.Cognisphere; +import dev.langchain4j.agentic.internal.AgentExecutor; +import io.serverlessworkflow.api.types.ForTaskConfiguration; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.List; +import java.util.UUID; +import java.util.function.ObjIntConsumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +public class LoopAgentsBuilder { + + private final FuncTaskItemListBuilder funcDelegate; + private final ForTaskFunction forTask; + + LoopAgentsBuilder() { + this.forTask = new ForTaskFunction(); + this.forTask.setFor(new ForTaskConfiguration()); + this.funcDelegate = new FuncTaskItemListBuilder(); + } + + private static void forEachIndexed(List list, ObjIntConsumer consumer) { + IntStream.range(0, list.size()).forEach(i -> consumer.accept(list.get(i), i)); + } + + public LoopAgentsBuilder subAgents(String baseName, Object... agents) { + List execs = AgentAdapters.toExecutors(agents); + forEachIndexed( + execs, + (exec, idx) -> + funcDelegate.callFn( + baseName + "-" + idx, fn -> fn.function(AgentAdapters.toFunction(exec)))); + return this; + } + + public LoopAgentsBuilder subAgents(Object... agents) { + return this.subAgents("agent-" + UUID.randomUUID(), agents); + } + + public LoopAgentsBuilder maxIterations(int maxIterations) { + this.forTask.withCollection(ignored -> IntStream.range(0, maxIterations).boxed().toList()); + return this; + } + + public LoopAgentsBuilder exitCondition(Predicate exitCondition) { + this.forTask.withWhile(AgentAdapters.toWhile(exitCondition)); + return this; + } + + public ForTaskFunction build() { + this.forTask.setDo(this.funcDelegate.build()); + return this.forTask; + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java new file mode 100644 index 00000000..8b9585db --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java @@ -0,0 +1,86 @@ +/* + * 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 org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Structural tests for AgentTaskItemListBuilder. */ +class AgentTaskItemListBuilderTest { + + @Test + @DisplayName("agent(name,obj) adds a CallTaskJava-backed TaskItem") + void testAgentAddsCallTask() { + AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); + Agents.MovieExpert agent = AgentsUtils.newMovieExpert(); + + b.agent("my-agent", agent); + + List items = b.build(); + assertThat(items).hasSize(1); + TaskItem item = items.get(0); + assertThat(item.getName()).isEqualTo("my-agent"); + + Task task = item.getTask(); + assertThat(task.getCallTask()).isInstanceOf(CallTaskJava.class); + } + + @Test + @DisplayName("sequence(name, agents...) expands to N CallTask items, in order") + void testSequence() { + AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); + Agents.MovieExpert a1 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a2 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a3 = AgentsUtils.newMovieExpert(); + + b.sequence("seq", a1, a2, a3); + + List items = b.build(); + assertThat(items).hasSize(3); + assertThat(items.get(0).getName()).isEqualTo("seq-0"); + assertThat(items.get(1).getName()).isEqualTo("seq-1"); + assertThat(items.get(2).getName()).isEqualTo("seq-2"); + + // All must be call branche + items.forEach(it -> assertThat(it.getTask().getCallTask().get()).isNotNull()); + } + + @Test + @DisplayName("loop(name, builder) produces a ForTaskFunction with inner call branche") + void testLoop() { + AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); + Agents.MovieExpert scorer = AgentsUtils.newMovieExpert(); + Agents.MovieExpert editor = AgentsUtils.newMovieExpert(); + + b.loop("rev-loop", loop -> loop.subAgents("inner", scorer, editor)); + + List items = b.build(); + assertThat(items).hasSize(1); + + TaskItem loopItem = items.get(0); + ForTaskFunction forFn = (ForTaskFunction) loopItem.getTask().getForTask(); + assertThat(forFn).isNotNull(); + assertThat(forFn.getDo()).hasSize(2); // scorer + editor inside + assertThat(forFn.getDo().get(0).getTask().getCallTask().get()).isNotNull(); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java new file mode 100644 index 00000000..51515654 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -0,0 +1,175 @@ +/* + * 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.AgentsUtils.newMovieExpert; +import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.Cognisphere; +import io.serverlessworkflow.api.types.ForkTask; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class AgenticWorkflowBuilderTest { + + @Test + public void verifyAgentCall() { + Agents.MovieExpert movieExpert = + spy( + AgentServices.agentBuilder(Agents.MovieExpert.class) + .outputName("movies") + .chatModel(BASE_MODEL) + .build()); + + Workflow workflow = + AgenticWorkflowBuilder.workflow() + .tasks(tasks -> tasks.agent("myAgent", movieExpert)) + .build(); + + assertNotNull(workflow); + assertEquals(1, workflow.getDo().size()); + assertInstanceOf(CallJava.class, workflow.getDo().get(0).getTask().getCallTask().get()); + } + + @Test + void sequenceAgents() { + Agents.MovieExpert movieExpert = newMovieExpert(); + Workflow wf = + AgenticWorkflowBuilder.workflow("seqFlow") + .tasks(d -> d.sequence("lineup", movieExpert, movieExpert)) + .build(); + + assertThat(wf.getDo()).hasSize(2); + assertThat(wf.getDo().get(0).getName()).isEqualTo("lineup-0"); + assertThat(wf.getDo().get(1).getName()).isEqualTo("lineup-1"); + wf.getDo() + .forEach( + ti -> { + assertThat(ti.getTask().getCallTask()).isNotNull(); + assertThat(ti.getTask().getCallTask().get()).isNotNull(); + }); + } + + @Test + void mixSpecAndAgent() { + Workflow wf = + AgenticWorkflowBuilder.workflow("mixFlow") + .tasks( + d -> + d.set("init", s -> s.expr("$.mood = 'comedy'")) + .agent("pickMovies", newMovieExpert()) + .set("done", "$.done = true")) + .build(); + + assertThat(wf.getDo()).hasSize(3); + assertThat(wf.getDo().get(0).getTask().getSetTask()).isNotNull(); + assertThat(wf.getDo().get(1).getTask().getCallTask().get()).isNotNull(); + assertThat(wf.getDo().get(2).getTask().getSetTask()).isNotNull(); + } + + @Test + void loopOnlyAgents() { + Agents.MovieExpert expert = newMovieExpert(); + + Workflow wf = + AgenticWorkflowBuilder.workflow().tasks(d -> d.loop(l -> l.subAgents(expert))).build(); + + assertNotNull(wf); + assertThat(wf.getDo()).hasSize(1); + + TaskItem ti = wf.getDo().get(0); + Task t = ti.getTask(); + assertThat(t.getForTask()).isInstanceOf(ForTaskFunction.class); + + ForTaskFunction fn = (ForTaskFunction) t.getForTask(); + assertNotNull(fn.getDo()); + assertThat(fn.getDo()).hasSize(1); + assertNotNull(fn.getDo().get(0).getTask().getCallTask().get()); + } + + @Test + void loopWithMaxIterationsAndExitCondition() { + Agents.MovieExpert expert = newMovieExpert(); + + AtomicInteger max = new AtomicInteger(4); + Predicate exit = + cog -> { + // stop when we already have at least one movie picked in state + var movies = cog.readState("movies", null); + return movies != null; + }; + + Workflow wf = + AgenticWorkflowBuilder.workflow("loop-ctrl") + .tasks( + d -> + d.loop( + "refineMovies", + l -> + l.maxIterations(max.get()) + .exitCondition(exit) + .subAgents("picker", expert))) + .build(); + + TaskItem ti = wf.getDo().get(0); + ForTaskFunction fn = (ForTaskFunction) ti.getTask().getForTask(); + + assertNotNull(fn.getCollection(), "Synthetic collection should exist for maxIterations"); + assertNotNull(fn.getWhilePredicate(), "While predicate set from exitCondition"); + assertThat(fn.getDo()).hasSize(1); + } + + @Test + @DisplayName("parallel() creates one ForkTask with N callFn branches") + void parallelAgents() { + Agents.MovieExpert a1 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a2 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a3 = AgentsUtils.newMovieExpert(); + + Workflow wf = + AgenticWorkflowBuilder.workflow("parallelFlow") + .tasks(d -> d.parallel("p", a1, a2, a3)) + .build(); + + assertThat(wf.getDo()).hasSize(1); + TaskItem top = wf.getDo().get(0); + Task task = top.getTask(); + assertThat(task.getForkTask()).isInstanceOf(ForkTask.class); + + ForkTask fork = task.getForkTask(); + assertThat(fork.getFork().getBranches()).hasSize(3); + + fork.getFork() + .getBranches() + .forEach( + branch -> { + assertThat(branch.getTask().getCallTask().get()).isInstanceOf(CallJava.class); + }); + } +} 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 new file mode 100644 index 00000000..81f71e4c --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -0,0 +1,37 @@ +/* + * 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.service.UserMessage; +import dev.langchain4j.service.V; +import java.util.List; + +public interface Agents { + + 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); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java new file mode 100644 index 00000000..a59f62e1 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -0,0 +1,34 @@ +/* + * 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.Models.BASE_MODEL; +import static org.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgentServices; + +public final class AgentsUtils { + + private AgentsUtils() {} + + public static Agents.MovieExpert newMovieExpert() { + return spy( + AgentServices.agentBuilder(Agents.MovieExpert.class) + .outputName("movies") + .chatModel(BASE_MODEL) + .build()); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java new file mode 100644 index 00000000..e06aafda --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java @@ -0,0 +1,32 @@ +/* + * 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.model.chat.ChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; +import java.time.Duration; + +public class Models { + static final ChatModel BASE_MODEL = + OllamaChatModel.builder() + .baseUrl("http://127.0.0.1:1143") + .modelName("qwen2.5:7b") + .timeout(Duration.ofMinutes(10)) + .temperature(0.0) + .logRequests(true) + .logResponses(true) + .build(); +} diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java new file mode 100644 index 00000000..131330fa --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java @@ -0,0 +1,86 @@ +/* + * 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.func; + +import io.serverlessworkflow.fluent.spec.HasDelegate; +import java.util.function.Consumer; + +/** + * Mixin that implements {@link FuncDoTaskFluent} by forwarding to another instance. + * + * @param concrete builder type + */ +public interface DelegatingFuncDoTaskFluent> + extends FuncDoTaskFluent, HasDelegate { + + @SuppressWarnings("unchecked") + default SELF self() { + return (SELF) this; + } + + @SuppressWarnings("unchecked") + private FuncDoTaskFluent d() { + return (FuncDoTaskFluent) this.delegate(); + } + + @Override + default SELF callFn(String name, Consumer cfg) { + d().callFn(name, cfg); + return self(); + } + + @Override + default SELF callFn(Consumer cfg) { + d().callFn(cfg); + return self(); + } + + @Override + default SELF forFn(String name, Consumer cfg) { + d().forFn(name, cfg); + return self(); + } + + @Override + default SELF forFn(Consumer cfg) { + d().forFn(cfg); + return self(); + } + + @Override + default SELF switchFn(String name, Consumer cfg) { + d().switchFn(name, cfg); + return self(); + } + + @Override + default SELF switchFn(Consumer cfg) { + d().switchFn(cfg); + return self(); + } + + @Override + default SELF forkFn(String name, Consumer cfg) { + d().forkFn(name, cfg); + return self(); + } + + @Override + default SELF forkFn(Consumer cfg) { + d().forkFn(cfg); + return self(); + } +} diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java index a8bcf08b..6c4c524d 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncCallTaskBuilder.java @@ -35,7 +35,7 @@ protected FuncCallTaskBuilder self() { return this; } - public FuncCallTaskBuilder fn(Function function) { + public FuncCallTaskBuilder function(Function function) { this.callTaskJava = new CallTaskJava(CallJava.function(function)); super.setTask(this.callTaskJava.getCallJava()); return this; diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java index ec721c1e..deefa099 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskBuilder.java @@ -16,47 +16,17 @@ package io.serverlessworkflow.fluent.func; import io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; -import java.util.function.Consumer; public class FuncDoTaskBuilder extends BaseDoTaskBuilder - implements FuncTransformations { + implements FuncTransformations, + DelegatingFuncDoTaskFluent { - FuncDoTaskBuilder() { + public FuncDoTaskBuilder() { super(new FuncTaskItemListBuilder()); } @Override - protected FuncDoTaskBuilder self() { - return this; - } - - public FuncDoTaskBuilder callFn(String name, Consumer consumer) { - this.innerListBuilder().callJava(name, consumer); - return this; - } - - public FuncDoTaskBuilder callFn(Consumer consumer) { - this.innerListBuilder().callJava(consumer); - return this; - } - - public FuncDoTaskBuilder forFn(String name, Consumer consumer) { - this.innerListBuilder().forFn(name, consumer); - return this; - } - - public FuncDoTaskBuilder forFn(Consumer consumer) { - this.innerListBuilder().forFn(consumer); - return this; - } - - public FuncDoTaskBuilder switchFn(String name, Consumer consumer) { - this.innerListBuilder().switchFn(name, consumer); - return this; - } - - public FuncDoTaskBuilder switchFn(Consumer consumer) { - this.innerListBuilder().switchFn(consumer); + public FuncDoTaskBuilder self() { return this; } } diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java new file mode 100644 index 00000000..9ec0cb39 --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java @@ -0,0 +1,37 @@ +/* + * 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.func; + +import java.util.function.Consumer; + +public interface FuncDoTaskFluent> { + + SELF callFn(String name, Consumer cfg); + + SELF callFn(Consumer cfg); + + SELF forFn(String name, Consumer cfg); + + SELF forFn(Consumer cfg); + + SELF switchFn(String name, Consumer cfg); + + SELF switchFn(Consumer cfg); + + SELF forkFn(String name, Consumer cfg); + + SELF forkFn(Consumer cfg); +} diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java new file mode 100644 index 00000000..7df66fab --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java @@ -0,0 +1,74 @@ +/* + * 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.func; + +import io.serverlessworkflow.api.types.ForkTask; +import io.serverlessworkflow.api.types.ForkTaskConfiguration; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.func.CallJava; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; + +public class FuncForkTaskBuilder extends TaskBaseBuilder + implements FuncTransformations { + + private final ForkTask forkTask; + private final List items; + + FuncForkTaskBuilder() { + this.forkTask = new ForkTask(); + this.forkTask.setFork(new ForkTaskConfiguration()); + this.items = new ArrayList<>(); + } + + @Override + protected FuncForkTaskBuilder self() { + return this; + } + + public FuncForkTaskBuilder branch(String name, Function function) { + this.items.add( + new TaskItem(name, new Task().withCallTask(new CallTaskJava(CallJava.function(function))))); + return this; + } + + public FuncForkTaskBuilder branch(Function function) { + return this.branch(UUID.randomUUID().toString(), function); + } + + public FuncForkTaskBuilder branches(Consumer consumer) { + final FuncTaskItemListBuilder builder = new FuncTaskItemListBuilder(); + consumer.accept(builder); + this.items.addAll(builder.build()); + return this; + } + + public FuncForkTaskBuilder compete(boolean compete) { + this.forkTask.getFork().setCompete(compete); + return this; + } + + public ForkTask build() { + this.forkTask.getFork().setBranches(this.items); + return forkTask; + } +} diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java index e11063da..13ce0c29 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncTaskItemListBuilder.java @@ -18,15 +18,21 @@ import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; +import java.util.List; import java.util.UUID; import java.util.function.Consumer; -public class FuncTaskItemListBuilder extends BaseTaskItemListBuilder { +public class FuncTaskItemListBuilder extends BaseTaskItemListBuilder + implements FuncDoTaskFluent { - FuncTaskItemListBuilder() { + public FuncTaskItemListBuilder() { super(); } + public FuncTaskItemListBuilder(final List list) { + super(list); + } + @Override protected FuncTaskItemListBuilder self() { return this; @@ -37,17 +43,20 @@ protected FuncTaskItemListBuilder newItemListBuilder() { return new FuncTaskItemListBuilder(); } - public FuncTaskItemListBuilder callJava(String name, Consumer consumer) { + @Override + public FuncTaskItemListBuilder callFn(String name, Consumer consumer) { this.requireNameAndConfig(name, consumer); final FuncCallTaskBuilder callTaskJavaBuilder = new FuncCallTaskBuilder(); consumer.accept(callTaskJavaBuilder); return addTaskItem(new TaskItem(name, new Task().withCallTask(callTaskJavaBuilder.build()))); } - public FuncTaskItemListBuilder callJava(Consumer consumer) { - return this.callJava(UUID.randomUUID().toString(), consumer); + @Override + public FuncTaskItemListBuilder callFn(Consumer consumer) { + return this.callFn(UUID.randomUUID().toString(), consumer); } + @Override public FuncTaskItemListBuilder forFn(String name, Consumer consumer) { this.requireNameAndConfig(name, consumer); final FuncForTaskBuilder forTaskJavaBuilder = new FuncForTaskBuilder(); @@ -55,10 +64,12 @@ public FuncTaskItemListBuilder forFn(String name, Consumer c return this.addTaskItem(new TaskItem(name, new Task().withForTask(forTaskJavaBuilder.build()))); } + @Override public FuncTaskItemListBuilder forFn(Consumer consumer) { return this.forFn(UUID.randomUUID().toString(), consumer); } + @Override public FuncTaskItemListBuilder switchFn(String name, Consumer consumer) { this.requireNameAndConfig(name, consumer); final FuncSwitchTaskBuilder funcSwitchTaskBuilder = new FuncSwitchTaskBuilder(); @@ -67,7 +78,22 @@ public FuncTaskItemListBuilder switchFn(String name, Consumer consumer) { return this.switchFn(UUID.randomUUID().toString(), consumer); } + + @Override + public FuncTaskItemListBuilder forkFn(Consumer cfg) { + return this.forkFn(UUID.randomUUID().toString(), cfg); + } + + @Override + public FuncTaskItemListBuilder forkFn(String name, Consumer cfg) { + this.requireNameAndConfig(name, cfg); + final FuncForkTaskBuilder forkTaskJavaBuilder = new FuncForkTaskBuilder(); + cfg.accept(forkTaskJavaBuilder); + return this.addTaskItem( + new TaskItem(name, new Task().withForkTask(forkTaskJavaBuilder.build()))); + } } diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncWorkflowBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncWorkflowBuilder.java index aad21591..686b1773 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncWorkflowBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncWorkflowBuilder.java @@ -22,7 +22,7 @@ public class FuncWorkflowBuilder extends BaseWorkflowBuilder implements FuncTransformations { - private FuncWorkflowBuilder(final String name, final String namespace, final String version) { + protected FuncWorkflowBuilder(final String name, final String namespace, final String version) { super(name, namespace, version); } diff --git a/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java b/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java index 529a9b8d..72cf6a68 100644 --- a/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java +++ b/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java @@ -27,7 +27,6 @@ import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.*; import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; -// if you reuse anything import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -166,7 +165,7 @@ void testJavaFunctionalIO() { ForTaskFunction fn = (ForTaskFunction) forTaskFnHolder.getForTask(); assertNotNull(fn); - // Inspect nested tasks inside the function loop + // Inspect nested branche inside the function loop List nested = fn.getDo(); assertEquals(1, nested.size()); TaskBase nestedTask = nested.get(0).getTask().getSetTask(); @@ -194,7 +193,7 @@ void testJavaFunctionalIO() { } @Test - @DisplayName("callJava task added and retains name + CallTask union") + @DisplayName("callFn task added and retains name + CallTask union") void testCallJavaTask() { Workflow wf = FuncWorkflowBuilder.workflow("callJavaFlow") @@ -214,20 +213,20 @@ void testCallJavaTask() { assertEquals("invokeHandler", ti.getName()); Task task = ti.getTask(); - assertNotNull(task.getCallTask(), "CallTask should be present for callJava"); + assertNotNull(task.getCallTask(), "CallTask should be present for callFn"); // Additional assertions if FuncCallTaskBuilder populates fields // e.g., assertEquals("com.acme.Handler", task.getCallTask().getCallJava().getClassName()); } @Test - @DisplayName("switchCaseFn (Java variant) coexists with spec tasks") + @DisplayName("switchCaseFn (Java variant) coexists with spec branche") void testSwitchCaseJava() { Workflow wf = FuncWorkflowBuilder.workflow("switchJava") .tasks( d -> d.set("prepare", s -> s.expr("$.ready = true")) - .switchC( + .switchCase( sw -> { // configure Java switch builder (cases / predicates) })) @@ -244,7 +243,7 @@ void testSwitchCaseJava() { } @Test - @DisplayName("Combined: spec set + java forE + callJava inside nested do") + @DisplayName("Combined: spec set + java forE + callFn inside nested do") void testCompositeScenario() { Workflow wf = FuncWorkflowBuilder.workflow("composite") @@ -257,7 +256,7 @@ void testCompositeScenario() { .tasks( inner -> inner - .callJava( + .callFn( cj -> { // customizing Java call }) diff --git a/fluent/pom.xml b/fluent/pom.xml index ff19f4d5..51aac994 100644 --- a/fluent/pom.xml +++ b/fluent/pom.xml @@ -35,12 +35,18 @@ serverlessworkflow-fluent-spec ${project.version} + + io.serverlessworkflow + serverlessworkflow-fluent-func + ${project.version} + spec func + agentic \ No newline at end of file diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java index d4c70aec..8e6a2410 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java @@ -16,128 +16,31 @@ package io.serverlessworkflow.fluent.spec; import io.serverlessworkflow.api.types.DoTask; -import java.util.function.Consumer; public abstract class BaseDoTaskBuilder< - TASK extends TaskBaseBuilder, LIST extends BaseTaskItemListBuilder> - extends TaskBaseBuilder { - private final DoTask doTask; - private final BaseTaskItemListBuilder taskItemListBuilder; + SELF extends BaseDoTaskBuilder, LIST extends BaseTaskItemListBuilder> + extends TaskBaseBuilder implements DelegatingDoTaskFluent { - protected BaseDoTaskBuilder(BaseTaskItemListBuilder taskItemListBuilder) { - this.doTask = new DoTask(); - this.taskItemListBuilder = taskItemListBuilder; - this.setTask(doTask); - } - - protected abstract TASK self(); - - protected LIST innerListBuilder() { - return (LIST) taskItemListBuilder; - } - - public TASK set(String name, Consumer itemsConfigurer) { - taskItemListBuilder.set(name, itemsConfigurer); - return self(); - } - - public TASK set(Consumer itemsConfigurer) { - taskItemListBuilder.set(itemsConfigurer); - return self(); - } - - public TASK set(String name, final String expr) { - taskItemListBuilder.set(name, s -> s.expr(expr)); - return self(); - } - - public TASK set(final String expr) { - taskItemListBuilder.set(expr); - return self(); - } - - public TASK forEach(String name, Consumer> itemsConfigurer) { - taskItemListBuilder.forEach(name, itemsConfigurer); - return self(); - } - - public TASK forEach(Consumer> itemsConfigurer) { - taskItemListBuilder.forEach(itemsConfigurer); - return self(); - } - - public TASK switchC(String name, Consumer itemsConfigurer) { - taskItemListBuilder.switchC(name, itemsConfigurer); - return self(); - } - - public TASK switchC(Consumer itemsConfigurer) { - taskItemListBuilder.switchC(itemsConfigurer); - return self(); - } - - public TASK raise(String name, Consumer itemsConfigurer) { - taskItemListBuilder.raise(name, itemsConfigurer); - return self(); - } - - public TASK raise(Consumer itemsConfigurer) { - taskItemListBuilder.raise(itemsConfigurer); - return self(); - } - - public TASK fork(String name, Consumer itemsConfigurer) { - taskItemListBuilder.fork(name, itemsConfigurer); - return self(); - } - - public TASK fork(Consumer itemsConfigurer) { - taskItemListBuilder.fork(itemsConfigurer); - return self(); - } - - public TASK listen(String name, Consumer itemsConfigurer) { - taskItemListBuilder.listen(name, itemsConfigurer); - return self(); - } - - public TASK listen(Consumer itemsConfigurer) { - taskItemListBuilder.listen(itemsConfigurer); - return self(); - } - - public TASK emit(String name, Consumer itemsConfigurer) { - taskItemListBuilder.emit(name, itemsConfigurer); - return self(); - } - - public TASK emit(Consumer itemsConfigurer) { - taskItemListBuilder.emit(itemsConfigurer); - return self(); - } - - public TASK tryC(String name, Consumer> itemsConfigurer) { - taskItemListBuilder.tryC(name, itemsConfigurer); - return self(); - } + private final DoTask doTask = new DoTask(); + private final BaseTaskItemListBuilder itemsListBuilder; - public TASK tryC(Consumer> itemsConfigurer) { - taskItemListBuilder.tryC(itemsConfigurer); - return self(); + protected BaseDoTaskBuilder(BaseTaskItemListBuilder itemsListBuilder) { + this.itemsListBuilder = itemsListBuilder; + setTask(doTask); } - public TASK callHTTP(String name, Consumer itemsConfigurer) { - taskItemListBuilder.callHTTP(name, itemsConfigurer); - return self(); + @SuppressWarnings("unchecked") + @Override + public LIST delegate() { + return (LIST) itemsListBuilder; } - public TASK callHTTP(Consumer itemsConfigurer) { - taskItemListBuilder.callHTTP(itemsConfigurer); - return self(); + public LIST list() { + return (LIST) itemsListBuilder; } public DoTask build() { - this.doTask.setDo(this.taskItemListBuilder.build()); - return this.doTask; + doTask.setDo(itemsListBuilder.build()); + return doTask; } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java index bb2a34dc..2233a418 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTaskItemListBuilder.java @@ -35,7 +35,8 @@ * * @param the concrete builder type */ -public abstract class BaseTaskItemListBuilder> { +public abstract class BaseTaskItemListBuilder> + implements DoTaskFluent { private final List list; @@ -43,21 +44,30 @@ public BaseTaskItemListBuilder() { this.list = new ArrayList<>(); } + public BaseTaskItemListBuilder(final List list) { + this.list = list; + } + protected abstract SELF self(); protected abstract SELF newItemListBuilder(); - protected SELF addTaskItem(TaskItem taskItem) { + protected final List mutableList() { + return this.list; + } + + protected final SELF addTaskItem(TaskItem taskItem) { Objects.requireNonNull(taskItem, "taskItem must not be null"); list.add(taskItem); return self(); } - protected void requireNameAndConfig(String name, Consumer cfg) { + protected final void requireNameAndConfig(String name, Consumer cfg) { Objects.requireNonNull(name, "Task name must not be null"); Objects.requireNonNull(cfg, "Configurer must not be null"); } + @Override public SELF set(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final SetTaskBuilder setBuilder = new SetTaskBuilder(); @@ -65,18 +75,22 @@ public SELF set(String name, Consumer itemsConfigurer) { return addTaskItem(new TaskItem(name, new Task().withSetTask(setBuilder.build()))); } + @Override public SELF set(Consumer itemsConfigurer) { return this.set(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF set(String name, final String expr) { return this.set(name, s -> s.expr(expr)); } + @Override public SELF set(final String expr) { return this.set(UUID.randomUUID().toString(), s -> s.expr(expr)); } + @Override public SELF forEach(String name, Consumer> itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final ForTaskBuilder forBuilder = new ForTaskBuilder<>(newItemListBuilder()); @@ -84,21 +98,25 @@ public SELF forEach(String name, Consumer> itemsConfigurer) return addTaskItem(new TaskItem(name, new Task().withForTask(forBuilder.build()))); } + @Override public SELF forEach(Consumer> itemsConfigurer) { return this.forEach(UUID.randomUUID().toString(), itemsConfigurer); } - public SELF switchC(String name, Consumer itemsConfigurer) { + @Override + public SELF switchCase(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final SwitchTaskBuilder switchBuilder = new SwitchTaskBuilder(); itemsConfigurer.accept(switchBuilder); return addTaskItem(new TaskItem(name, new Task().withSwitchTask(switchBuilder.build()))); } - public SELF switchC(Consumer itemsConfigurer) { - return this.switchC(UUID.randomUUID().toString(), itemsConfigurer); + @Override + public SELF switchCase(Consumer itemsConfigurer) { + return this.switchCase(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF raise(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final RaiseTaskBuilder raiseBuilder = new RaiseTaskBuilder(); @@ -106,10 +124,12 @@ public SELF raise(String name, Consumer itemsConfigurer) { return addTaskItem(new TaskItem(name, new Task().withRaiseTask(raiseBuilder.build()))); } + @Override public SELF raise(Consumer itemsConfigurer) { return this.raise(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF fork(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final ForkTaskBuilder forkBuilder = new ForkTaskBuilder(); @@ -117,10 +137,12 @@ public SELF fork(String name, Consumer itemsConfigurer) { return addTaskItem(new TaskItem(name, new Task().withForkTask(forkBuilder.build()))); } + @Override public SELF fork(Consumer itemsConfigurer) { return this.fork(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF listen(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final ListenTaskBuilder listenBuilder = new ListenTaskBuilder(); @@ -128,10 +150,12 @@ public SELF listen(String name, Consumer itemsConfigurer) { return addTaskItem(new TaskItem(name, new Task().withListenTask(listenBuilder.build()))); } + @Override public SELF listen(Consumer itemsConfigurer) { return this.listen(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF emit(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final EmitTaskBuilder emitBuilder = new EmitTaskBuilder(); @@ -139,21 +163,25 @@ public SELF emit(String name, Consumer itemsConfigurer) { return addTaskItem(new TaskItem(name, new Task().withEmitTask(emitBuilder.build()))); } + @Override public SELF emit(Consumer itemsConfigurer) { return this.emit(UUID.randomUUID().toString(), itemsConfigurer); } - public SELF tryC(String name, Consumer> itemsConfigurer) { + @Override + public SELF tryCatch(String name, Consumer> itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final TryTaskBuilder tryBuilder = new TryTaskBuilder<>(this.newItemListBuilder()); itemsConfigurer.accept(tryBuilder); return addTaskItem(new TaskItem(name, new Task().withTryTask(tryBuilder.build()))); } - public SELF tryC(Consumer> itemsConfigurer) { - return this.tryC(UUID.randomUUID().toString(), itemsConfigurer); + @Override + public SELF tryCatch(Consumer> itemsConfigurer) { + return this.tryCatch(UUID.randomUUID().toString(), itemsConfigurer); } + @Override public SELF callHTTP(String name, Consumer itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); final CallHTTPTaskBuilder callHTTPBuilder = new CallHTTPTaskBuilder(); @@ -163,6 +191,7 @@ public SELF callHTTP(String name, Consumer itemsConfigurer) name, new Task().withCallTask(new CallTask().withCallHTTP(callHTTPBuilder.build())))); } + @Override public SELF callHTTP(Consumer itemsConfigurer) { return this.callHTTP(UUID.randomUUID().toString(), itemsConfigurer); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java new file mode 100644 index 00000000..bfaa6817 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java @@ -0,0 +1,163 @@ +/* + * 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.spec; + +import java.util.function.Consumer; + +/** + * Mixin that implements {@link DoTaskFluent} by delegating to another instance. + * + * @param the concrete delegating type + * @param the list-builder type used by nested constructs like for/try + */ +public interface DelegatingDoTaskFluent< + SELF extends DelegatingDoTaskFluent, LIST extends BaseTaskItemListBuilder> + extends DoTaskFluent, HasDelegate { + + @SuppressWarnings("unchecked") + default SELF self() { + return (SELF) this; + } + + LIST list(); + + @SuppressWarnings("unchecked") + private DoTaskFluent d() { + return (DoTaskFluent) this.delegate(); + } + + /* ---------- Forwarders ---------- */ + + @Override + default SELF set(String name, Consumer cfg) { + d().set(name, cfg); + return self(); + } + + @Override + default SELF set(Consumer cfg) { + d().set(cfg); + return self(); + } + + @Override + default SELF set(String name, String expr) { + d().set(name, expr); + return self(); + } + + @Override + default SELF set(String expr) { + d().set(expr); + return self(); + } + + @Override + default SELF forEach(String name, Consumer> cfg) { + d().forEach(name, cfg); + return self(); + } + + @Override + default SELF forEach(Consumer> cfg) { + d().forEach(cfg); + return self(); + } + + @Override + default SELF switchCase(String name, Consumer cfg) { + d().switchCase(name, cfg); + return self(); + } + + @Override + default SELF switchCase(Consumer cfg) { + d().switchCase(cfg); + return self(); + } + + @Override + default SELF raise(String name, Consumer cfg) { + d().raise(name, cfg); + return self(); + } + + @Override + default SELF raise(Consumer cfg) { + d().raise(cfg); + return self(); + } + + @Override + default SELF fork(String name, Consumer cfg) { + d().fork(name, cfg); + return self(); + } + + @Override + default SELF fork(Consumer cfg) { + d().fork(cfg); + return self(); + } + + @Override + default SELF listen(String name, Consumer cfg) { + d().listen(name, cfg); + return self(); + } + + @Override + default SELF listen(Consumer cfg) { + d().listen(cfg); + return self(); + } + + @Override + default SELF emit(String name, Consumer cfg) { + d().emit(name, cfg); + return self(); + } + + @Override + default SELF emit(Consumer cfg) { + d().emit(cfg); + return self(); + } + + @Override + default SELF tryCatch(String name, Consumer> cfg) { + d().tryCatch(name, cfg); + return self(); + } + + @Override + default SELF tryCatch(Consumer> cfg) { + d().tryCatch(cfg); + return self(); + } + + @Override + default SELF callHTTP(String name, Consumer cfg) { + d().callHTTP(name, cfg); + return self(); + } + + @Override + default SELF callHTTP(Consumer cfg) { + d().callHTTP(cfg); + return self(); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java index a9d4f6cb..3b1a768c 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskBuilder.java @@ -22,7 +22,7 @@ public class DoTaskBuilder extends BaseDoTaskBuilderCNCF + * DSL Reference - Do + * @param The TaskBaseBuilder constructor that sub-tasks will build + * @param The specialized BaseTaskItemListBuilder for sub-tasks that require a list of + * sub-tasks, such as `for`. + */ +public interface DoTaskFluent< + SELF extends DoTaskFluent, LIST extends BaseTaskItemListBuilder> { + + SELF set(String name, Consumer itemsConfigurer); + + SELF set(Consumer itemsConfigurer); + + SELF set(String name, final String expr); + + SELF set(final String expr); + + SELF forEach(String name, Consumer> itemsConfigurer); + + SELF forEach(Consumer> itemsConfigurer); + + SELF switchCase(String name, Consumer itemsConfigurer); + + SELF switchCase(Consumer itemsConfigurer); + + SELF raise(String name, Consumer itemsConfigurer); + + SELF raise(Consumer itemsConfigurer); + + SELF fork(String name, Consumer itemsConfigurer); + + SELF fork(Consumer itemsConfigurer); + + SELF listen(String name, Consumer itemsConfigurer); + + SELF listen(Consumer itemsConfigurer); + + SELF emit(String name, Consumer itemsConfigurer); + + SELF emit(Consumer itemsConfigurer); + + SELF tryCatch(String name, Consumer> itemsConfigurer); + + SELF tryCatch(Consumer> itemsConfigurer); + + SELF callHTTP(String name, Consumer itemsConfigurer); + + SELF callHTTP(Consumer itemsConfigurer); + + // ----- shortcuts/aliases + + default SELF sc(String name, Consumer cfg) { + return switchCase(name, cfg); + } + + default SELF sc(Consumer cfg) { + return switchCase(cfg); + } + + default SELF tc(String name, Consumer> cfg) { + return tryCatch(name, cfg); + } + + default SELF tc(Consumer> cfg) { + return tryCatch(cfg); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/HasDelegate.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/HasDelegate.java new file mode 100644 index 00000000..f0b1e0b9 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/HasDelegate.java @@ -0,0 +1,21 @@ +/* + * 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.spec; + +public interface HasDelegate { + + Object delegate(); +} diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java index 49d41dbb..dbb9f1d3 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java @@ -128,7 +128,7 @@ void testDoTaskMultipleTypes() { d -> d.set("init", s -> s.expr("$.init = true")) .forEach("items", f -> f.each("item").in("$.list")) - .switchC( + .switchCase( "choice", sw -> { // no-op configuration @@ -258,7 +258,7 @@ void testDoTaskTryCatchWithRetry() { WorkflowBuilder.workflow("flowTry") .tasks( d -> - d.tryC( + d.tryCatch( "tryBlock", t -> t.tryHandler(tb -> tb.set("init", s -> s.expr("$.start = true"))) @@ -306,7 +306,7 @@ void testDoTaskTryCatchErrorsFiltering() { WorkflowBuilder.workflow("flowCatch") .tasks( d -> - d.tryC( + d.tryCatch( "tryBlock", t -> t.tryHandler(tb -> tb.set("foo", s -> s.expr("$.foo = 'bar'"))) diff --git a/pom.xml b/pom.xml index b456e6a4..428e82bb 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,11 @@ slf4j-api ${version.org.slf4j} + + org.slf4j + slf4j-simple + ${version.org.slf4j} + io.serverlessworkflow serverlessworkflow-api