From fd4ba7de9988c68bc81e059a7889fbf5f37d548f Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 22 Jul 2025 18:56:21 -0400 Subject: [PATCH 1/5] Introduce Agentic DSL Signed-off-by: Ricardo Zanini --- .../workflow/impl/FluentDSLCallTest.java | 4 +- fluent/agentic/pom.xml | 69 ++++++++ .../fluent/agentic/AgentAdapters.java | 37 ++++ .../fluent/agentic/AgentDoTaskBuilder.java | 46 +++++ .../fluent/agentic/AgentDoTaskFluent.java | 21 +++ .../agentic/AgentTaskItemListBuilder.java | 36 ++++ .../agentic/AgenticWorkflowBuilder.java | 78 +++++++++ .../agentic/DelegatingAgentDoTaskFluent.java | 33 ++++ .../agentic/AgenticWorkflowBuilderTest.java | 47 +++++ .../fluent/agentic/Agents.java | 37 ++++ .../fluent/agentic/Models.java | 32 ++++ .../func/DelegatingFuncDoTaskFluent.java | 71 ++++++++ .../fluent/func/FuncCallTaskBuilder.java | 2 +- .../fluent/func/FuncDoTaskBuilder.java | 41 ++--- .../fluent/func/FuncDoTaskFluent.java | 33 ++++ .../fluent/func/FuncTaskItemListBuilder.java | 22 ++- .../fluent/func/FuncWorkflowBuilder.java | 2 +- .../fluent/func/JavaWorkflowBuilderTest.java | 11 +- fluent/pom.xml | 6 + .../fluent/spec/BaseDoTaskBuilder.java | 127 ++------------ .../fluent/spec/BaseTaskItemListBuilder.java | 45 ++++- .../fluent/spec/DelegatingDoTaskFluent.java | 160 ++++++++++++++++++ .../fluent/spec/DoTaskBuilder.java | 2 +- .../fluent/spec/DoTaskFluent.java | 90 ++++++++++ .../fluent/spec/WorkflowBuilderTest.java | 6 +- pom.xml | 5 + 26 files changed, 891 insertions(+), 172 deletions(-) create mode 100644 fluent/agentic/pom.xml create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java create mode 100644 fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java create mode 100644 fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DoTaskFluent.java 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..49d0e67c --- /dev/null +++ b/fluent/agentic/pom.xml @@ -0,0 +1,69 @@ + + + 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 + + + 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..1887db65 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.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 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 java.util.List; +import java.util.function.Function; +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 toFn(AgentExecutor exec) { + return exec::invoke; + } +} 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..ac15eeed --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.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 io.serverlessworkflow.fluent.func.FuncDoTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; + +public class AgentDoTaskBuilder extends FuncDoTaskBuilder + implements DelegatingAgentDoTaskFluent { + + private AgentDoTaskBuilder(AgentTaskItemListBuilder agentTaskItemListBuilder) { + super(agentTaskItemListBuilder); + } + + static AgentDoTaskBuilder wrap(FuncDoTaskBuilder base) { + FuncTaskItemListBuilder funcList = base.internalDelegate(); + AgentTaskItemListBuilder agentList = + (funcList instanceof AgentTaskItemListBuilder al) + ? al + : new AgentTaskItemListBuilder(funcList.getInternalList()); + return new AgentDoTaskBuilder(agentList); + } + + @Override + public AgentDoTaskFluent agentInternalDelegate() { + return (AgentDoTaskFluent) super.internalDelegate(); + } + + @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..bd349a82 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.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.agentic; + +public interface AgentDoTaskFluent> { + + SELF agent(String name, Object agent); +} 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..ca6db4c2 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -0,0 +1,36 @@ +/* + * 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.TaskItem; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.List; + +public class AgentTaskItemListBuilder extends FuncTaskItemListBuilder + implements AgentDoTaskFluent { + + AgentTaskItemListBuilder(final List list) { + super(list); + } + + @Override + public AgentTaskItemListBuilder agent(String name, Object agent) { + AgentExecutor exec = AgentAdapters.toExecutors(agent).get(0); + this.callFn(name, fn -> fn.function(AgentAdapters.toFn(exec))); + 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..30f1f9bc --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java @@ -0,0 +1,78 @@ +/* + * 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.api.types.Workflow; +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.fluent.spec.DocumentBuilder; +import io.serverlessworkflow.fluent.spec.InputBuilder; +import io.serverlessworkflow.fluent.spec.OutputBuilder; +import io.serverlessworkflow.fluent.spec.UseBuilder; +import java.util.function.Consumer; + +public final class AgenticWorkflowBuilder { + + private final FuncWorkflowBuilder delegate; + + AgenticWorkflowBuilder(final FuncWorkflowBuilder delegate) { + this.delegate = delegate; + } + + public static AgenticWorkflowBuilder workflow() { + return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow()); + } + + public static AgenticWorkflowBuilder workflow(String name) { + return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow(name)); + } + + public static AgenticWorkflowBuilder workflow(String name, String ns) { + return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow(name, ns)); + } + + public AgenticWorkflowBuilder document(Consumer c) { + delegate.document(c); + return this; + } + + public AgenticWorkflowBuilder input(Consumer c) { + delegate.input(c); + return this; + } + + public AgenticWorkflowBuilder output(Consumer c) { + delegate.output(c); + return this; + } + + public AgenticWorkflowBuilder use(Consumer c) { + delegate.use(c); + return this; + } + + public AgenticWorkflowBuilder tasks(Consumer c) { + delegate.tasks( + funcDo -> { + AgentDoTaskBuilder agentDoTaskBuilder = AgentDoTaskBuilder.wrap(funcDo); + c.accept(agentDoTaskBuilder); + }); + return this; + } + + public Workflow build() { + return delegate.build(); + } +} 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..e15abf58 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -0,0 +1,33 @@ +/* + * 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; + +public interface DelegatingAgentDoTaskFluent> + extends AgentDoTaskFluent { + + /** Return the underlying functional ops delegate. */ + AgentDoTaskFluent agentInternalDelegate(); + + @SuppressWarnings("unchecked") + default SELF self() { + return (SELF) this; + } + + default SELF agent(String name, Object agent) { + agentInternalDelegate().agent(name, agent); + return (SELF) this; + } +} 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..a6f0cca5 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -0,0 +1,47 @@ +/* + * 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.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 io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.api.types.func.CallJava; +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); + assertInstanceOf(CallJava.class, workflow.getDo().get(0).getTask().getCallTask().get()); + } +} 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..298b8009 --- /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/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..b14ccc51 --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java @@ -0,0 +1,71 @@ +/* + * 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; + +/** + * Mixin that implements {@link FuncDoTaskFluent} by forwarding to another instance. + * + * @param concrete builder type + */ +public interface DelegatingFuncDoTaskFluent> + extends FuncDoTaskFluent { + + /** Return the underlying functional ops delegate. */ + FuncDoTaskFluent funcInternalDelegate(); + + @SuppressWarnings("unchecked") + default SELF self() { + return (SELF) this; + } + + @Override + default SELF callFn(String name, Consumer cfg) { + funcInternalDelegate().callFn(name, cfg); + return self(); + } + + @Override + default SELF callFn(Consumer cfg) { + funcInternalDelegate().callFn(cfg); + return self(); + } + + @Override + default SELF forFn(String name, Consumer cfg) { + funcInternalDelegate().forFn(name, cfg); + return self(); + } + + @Override + default SELF forFn(Consumer cfg) { + funcInternalDelegate().forFn(cfg); + return self(); + } + + @Override + default SELF switchFn(String name, Consumer cfg) { + funcInternalDelegate().switchFn(name, cfg); + return self(); + } + + @Override + default SELF switchFn(Consumer cfg) { + funcInternalDelegate().switchFn(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..e55ab660 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,26 @@ 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 { + + protected FuncDoTaskBuilder(FuncTaskItemListBuilder listBuilder) { + super(listBuilder); + } FuncDoTaskBuilder() { super(new FuncTaskItemListBuilder()); } @Override - protected FuncDoTaskBuilder self() { - return this; - } - - public FuncDoTaskBuilder callFn(String name, Consumer consumer) { - this.innerListBuilder().callJava(name, consumer); + public FuncDoTaskBuilder self() { 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); - return this; + @Override + public FuncDoTaskFluent funcInternalDelegate() { + return internalDelegate(); } } 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..ae65a478 --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java @@ -0,0 +1,33 @@ +/* + * 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); +} 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..063ab0a9 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() { + protected FuncTaskItemListBuilder() { super(); } + protected 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,6 +78,7 @@ public FuncTaskItemListBuilder switchFn(String name, Consumer consumer) { return this.switchFn(UUID.randomUUID().toString(), consumer); } 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..7a931994 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) { + 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..3aa1e092 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; @@ -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,7 +213,7 @@ 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()); } @@ -227,7 +226,7 @@ void testSwitchCaseJava() { .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..d4dfc150 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,27 @@ 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(); - } - - public TASK tryC(Consumer> itemsConfigurer) { - taskItemListBuilder.tryC(itemsConfigurer); - return self(); - } + private final DoTask doTask = new DoTask(); + private final BaseTaskItemListBuilder itemsListBuilder; - public TASK callHTTP(String name, Consumer itemsConfigurer) { - taskItemListBuilder.callHTTP(name, itemsConfigurer); - return self(); + protected BaseDoTaskBuilder(BaseTaskItemListBuilder itemsListBuilder) { + this.itemsListBuilder = itemsListBuilder; + setTask(doTask); } - public TASK callHTTP(Consumer itemsConfigurer) { - taskItemListBuilder.callHTTP(itemsConfigurer); - return self(); + @SuppressWarnings("unchecked") + @Override + public LIST internalDelegate() { + 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..c34c00af 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,11 +44,19 @@ public BaseTaskItemListBuilder() { this.list = new ArrayList<>(); } + protected BaseTaskItemListBuilder(List list) { + this.list = list; + } + + public final List getInternalList() { + return this.list; + } + protected abstract SELF self(); protected abstract SELF newItemListBuilder(); - protected SELF addTaskItem(TaskItem taskItem) { + protected final SELF addTaskItem(TaskItem taskItem) { Objects.requireNonNull(taskItem, "taskItem must not be null"); list.add(taskItem); return self(); @@ -58,6 +67,7 @@ protected void requireNameAndConfig(String name, Consumer cfg) { 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..15f2dfbd --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java @@ -0,0 +1,160 @@ +/* + * 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 { + + /** The underlying DoTaskFluent to forward to. */ + DoTaskFluent internalDelegate(); + + /** Convenience cast; implementors just return (SELF) this in practice. */ + @SuppressWarnings("unchecked") + default SELF self() { + return (SELF) this; + } + + /* ---------- Forwarders ---------- */ + + @Override + default SELF set(String name, Consumer cfg) { + internalDelegate().set(name, cfg); + return self(); + } + + @Override + default SELF set(Consumer cfg) { + internalDelegate().set(cfg); + return self(); + } + + @Override + default SELF set(String name, String expr) { + internalDelegate().set(name, expr); + return self(); + } + + @Override + default SELF set(String expr) { + internalDelegate().set(expr); + return self(); + } + + @Override + default SELF forEach(String name, Consumer> cfg) { + internalDelegate().forEach(name, cfg); + return self(); + } + + @Override + default SELF forEach(Consumer> cfg) { + internalDelegate().forEach(cfg); + return self(); + } + + @Override + default SELF switchCase(String name, Consumer cfg) { + internalDelegate().switchCase(name, cfg); + return self(); + } + + @Override + default SELF switchCase(Consumer cfg) { + internalDelegate().switchCase(cfg); + return self(); + } + + @Override + default SELF raise(String name, Consumer cfg) { + internalDelegate().raise(name, cfg); + return self(); + } + + @Override + default SELF raise(Consumer cfg) { + internalDelegate().raise(cfg); + return self(); + } + + @Override + default SELF fork(String name, Consumer cfg) { + internalDelegate().fork(name, cfg); + return self(); + } + + @Override + default SELF fork(Consumer cfg) { + internalDelegate().fork(cfg); + return self(); + } + + @Override + default SELF listen(String name, Consumer cfg) { + internalDelegate().listen(name, cfg); + return self(); + } + + @Override + default SELF listen(Consumer cfg) { + internalDelegate().listen(cfg); + return self(); + } + + @Override + default SELF emit(String name, Consumer cfg) { + internalDelegate().emit(name, cfg); + return self(); + } + + @Override + default SELF emit(Consumer cfg) { + internalDelegate().emit(cfg); + return self(); + } + + @Override + default SELF tryCatch(String name, Consumer> cfg) { + internalDelegate().tryCatch(name, cfg); + return self(); + } + + @Override + default SELF tryCatch(Consumer> cfg) { + internalDelegate().tryCatch(cfg); + return self(); + } + + @Override + default SELF callHTTP(String name, Consumer cfg) { + internalDelegate().callHTTP(name, cfg); + return self(); + } + + @Override + default SELF callHTTP(Consumer cfg) { + internalDelegate().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/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 From 24c94d06dc7303bc9f8e69474248ff775bf19b73 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 23 Jul 2025 15:42:04 -0400 Subject: [PATCH 2/5] Refactor delegates Signed-off-by: Ricardo Zanini --- .../fluent/agentic/AgentAdapters.java | 2 +- .../fluent/agentic/AgentDoTaskBuilder.java | 24 ++------ .../agentic/AgentTaskItemListBuilder.java | 29 +++++++--- .../agentic/AgenticWorkflowBuilder.java | 57 +++++-------------- .../agentic/DelegatingAgentDoTaskFluent.java | 15 ++--- .../agentic/AgenticWorkflowBuilderTest.java | 2 + .../func/DelegatingFuncDoTaskFluent.java | 23 ++++---- .../fluent/func/FuncDoTaskBuilder.java | 11 +--- .../fluent/func/FuncTaskItemListBuilder.java | 4 +- .../fluent/func/FuncWorkflowBuilder.java | 2 +- .../fluent/spec/BaseDoTaskBuilder.java | 6 +- .../fluent/spec/BaseTaskItemListBuilder.java | 12 ++-- .../fluent/spec/DelegatingDoTaskFluent.java | 53 +++++++++-------- .../fluent/spec/HasDelegate.java | 21 +++++++ 14 files changed, 129 insertions(+), 132 deletions(-) create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/HasDelegate.java 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 index 1887db65..b65c9a12 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java @@ -31,7 +31,7 @@ public static List toExecutors(Object... agents) { return agentsToExecutors(Stream.of(agents).map(AgentInstance.class::cast).toList()); } - public static Function toFn(AgentExecutor exec) { + public static Function toFunction(AgentExecutor exec) { return exec::invoke; } } 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 index ac15eeed..3d9b1cbc 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java @@ -15,28 +15,14 @@ */ package io.serverlessworkflow.fluent.agentic; -import io.serverlessworkflow.fluent.func.FuncDoTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; -public class AgentDoTaskBuilder extends FuncDoTaskBuilder +public class AgentDoTaskBuilder + extends BaseDoTaskBuilder implements DelegatingAgentDoTaskFluent { - private AgentDoTaskBuilder(AgentTaskItemListBuilder agentTaskItemListBuilder) { - super(agentTaskItemListBuilder); - } - - static AgentDoTaskBuilder wrap(FuncDoTaskBuilder base) { - FuncTaskItemListBuilder funcList = base.internalDelegate(); - AgentTaskItemListBuilder agentList = - (funcList instanceof AgentTaskItemListBuilder al) - ? al - : new AgentTaskItemListBuilder(funcList.getInternalList()); - return new AgentDoTaskBuilder(agentList); - } - - @Override - public AgentDoTaskFluent agentInternalDelegate() { - return (AgentDoTaskFluent) super.internalDelegate(); + AgentDoTaskBuilder() { + super(new AgentTaskItemListBuilder()); } @Override 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 index ca6db4c2..2d86faf7 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -15,22 +15,35 @@ */ package io.serverlessworkflow.fluent.agentic; -import dev.langchain4j.agentic.internal.AgentExecutor; -import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; -import java.util.List; +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; -public class AgentTaskItemListBuilder extends FuncTaskItemListBuilder +public class AgentTaskItemListBuilder extends BaseTaskItemListBuilder implements AgentDoTaskFluent { - AgentTaskItemListBuilder(final List list) { - super(list); + 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) { - AgentExecutor exec = AgentAdapters.toExecutors(agent).get(0); - this.callFn(name, fn -> fn.function(AgentAdapters.toFn(exec))); + AgentAdapters.toExecutors(agent) + .forEach( + exec -> + this.funcDelegate.callFn(name, fn -> fn.function(AgentAdapters.toFunction(exec)))); + return this; + } + + @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 index 30f1f9bc..cef01011 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilder.java @@ -15,64 +15,37 @@ */ package io.serverlessworkflow.fluent.agentic; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; -import io.serverlessworkflow.fluent.spec.DocumentBuilder; -import io.serverlessworkflow.fluent.spec.InputBuilder; -import io.serverlessworkflow.fluent.spec.OutputBuilder; -import io.serverlessworkflow.fluent.spec.UseBuilder; -import java.util.function.Consumer; +import static java.lang.constant.ConstantDescs.DEFAULT_NAME; -public final class AgenticWorkflowBuilder { +import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; - private final FuncWorkflowBuilder delegate; +public final class AgenticWorkflowBuilder + extends BaseWorkflowBuilder< + AgenticWorkflowBuilder, AgentDoTaskBuilder, AgentTaskItemListBuilder> { - AgenticWorkflowBuilder(final FuncWorkflowBuilder delegate) { - this.delegate = delegate; + AgenticWorkflowBuilder(final String name, final String namespace, final String version) { + super(name, namespace, version); } public static AgenticWorkflowBuilder workflow() { - return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow()); + return new AgenticWorkflowBuilder(DEFAULT_NAME, DEFAULT_NAMESPACE, DEFAULT_VERSION); } public static AgenticWorkflowBuilder workflow(String name) { - return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow(name)); + return new AgenticWorkflowBuilder(name, DEFAULT_NAMESPACE, DEFAULT_VERSION); } public static AgenticWorkflowBuilder workflow(String name, String ns) { - return new AgenticWorkflowBuilder(FuncWorkflowBuilder.workflow(name, ns)); + return new AgenticWorkflowBuilder(name, ns, DEFAULT_VERSION); } - public AgenticWorkflowBuilder document(Consumer c) { - delegate.document(c); - return this; - } - - public AgenticWorkflowBuilder input(Consumer c) { - delegate.input(c); - return this; - } - - public AgenticWorkflowBuilder output(Consumer c) { - delegate.output(c); - return this; - } - - public AgenticWorkflowBuilder use(Consumer c) { - delegate.use(c); - return this; + @Override + protected AgentDoTaskBuilder newDo() { + return new AgentDoTaskBuilder(); } - public AgenticWorkflowBuilder tasks(Consumer c) { - delegate.tasks( - funcDo -> { - AgentDoTaskBuilder agentDoTaskBuilder = AgentDoTaskBuilder.wrap(funcDo); - c.accept(agentDoTaskBuilder); - }); + @Override + protected AgenticWorkflowBuilder self() { return this; } - - public Workflow build() { - return delegate.build(); - } } 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 index e15abf58..4fc23f14 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -15,19 +15,20 @@ */ package io.serverlessworkflow.fluent.agentic; -public interface DelegatingAgentDoTaskFluent> - extends AgentDoTaskFluent { +import io.serverlessworkflow.fluent.func.DelegatingFuncDoTaskFluent; +import io.serverlessworkflow.fluent.spec.HasDelegate; - /** Return the underlying functional ops delegate. */ - AgentDoTaskFluent agentInternalDelegate(); +public interface DelegatingAgentDoTaskFluent> + extends AgentDoTaskFluent, DelegatingFuncDoTaskFluent, HasDelegate { @SuppressWarnings("unchecked") - default SELF self() { - return (SELF) this; + private AgentDoTaskFluent d() { + return (AgentDoTaskFluent) this.delegate(); } + @SuppressWarnings("unchecked") default SELF agent(String name, Object agent) { - agentInternalDelegate().agent(name, agent); + d().agent(name, agent); return (SELF) this; } } 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 index a6f0cca5..755d9f3a 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.agentic; import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; +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; @@ -42,6 +43,7 @@ public void verifyAgentCall() { .build(); assertNotNull(workflow); + assertEquals(1, workflow.getDo().size()); assertInstanceOf(CallJava.class, workflow.getDo().get(0).getTask().getCallTask().get()); } } 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 index b14ccc51..edb8375d 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.fluent.func; +import io.serverlessworkflow.fluent.spec.HasDelegate; import java.util.function.Consumer; /** @@ -23,49 +24,51 @@ * @param concrete builder type */ public interface DelegatingFuncDoTaskFluent> - extends FuncDoTaskFluent { - - /** Return the underlying functional ops delegate. */ - FuncDoTaskFluent funcInternalDelegate(); + 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) { - funcInternalDelegate().callFn(name, cfg); + d().callFn(name, cfg); return self(); } @Override default SELF callFn(Consumer cfg) { - funcInternalDelegate().callFn(cfg); + d().callFn(cfg); return self(); } @Override default SELF forFn(String name, Consumer cfg) { - funcInternalDelegate().forFn(name, cfg); + d().forFn(name, cfg); return self(); } @Override default SELF forFn(Consumer cfg) { - funcInternalDelegate().forFn(cfg); + d().forFn(cfg); return self(); } @Override default SELF switchFn(String name, Consumer cfg) { - funcInternalDelegate().switchFn(name, cfg); + d().switchFn(name, cfg); return self(); } @Override default SELF switchFn(Consumer cfg) { - funcInternalDelegate().switchFn(cfg); + d().switchFn(cfg); return self(); } } 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 e55ab660..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 @@ -21,11 +21,7 @@ public class FuncDoTaskBuilder extends BaseDoTaskBuilder, DelegatingFuncDoTaskFluent { - protected FuncDoTaskBuilder(FuncTaskItemListBuilder listBuilder) { - super(listBuilder); - } - - FuncDoTaskBuilder() { + public FuncDoTaskBuilder() { super(new FuncTaskItemListBuilder()); } @@ -33,9 +29,4 @@ protected FuncDoTaskBuilder(FuncTaskItemListBuilder listBuilder) { public FuncDoTaskBuilder self() { return this; } - - @Override - public FuncDoTaskFluent funcInternalDelegate() { - return internalDelegate(); - } } 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 063ab0a9..9bc40f9a 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 @@ -25,11 +25,11 @@ public class FuncTaskItemListBuilder extends BaseTaskItemListBuilder implements FuncDoTaskFluent { - protected FuncTaskItemListBuilder() { + public FuncTaskItemListBuilder() { super(); } - protected FuncTaskItemListBuilder(final List list) { + public FuncTaskItemListBuilder(final List list) { super(list); } 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 7a931994..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 { - 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/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseDoTaskBuilder.java index d4dfc150..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 @@ -31,7 +31,11 @@ protected BaseDoTaskBuilder(BaseTaskItemListBuilder itemsListBuilder) { @SuppressWarnings("unchecked") @Override - public LIST internalDelegate() { + public LIST delegate() { + return (LIST) itemsListBuilder; + } + + public LIST list() { return (LIST) itemsListBuilder; } 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 c34c00af..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 @@ -44,25 +44,25 @@ public BaseTaskItemListBuilder() { this.list = new ArrayList<>(); } - protected BaseTaskItemListBuilder(List list) { + public BaseTaskItemListBuilder(final List list) { this.list = list; } - public final List getInternalList() { - return this.list; - } - protected abstract SELF self(); protected abstract SELF newItemListBuilder(); + 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"); } 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 index 15f2dfbd..bfaa6817 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/DelegatingDoTaskFluent.java @@ -25,136 +25,139 @@ */ public interface DelegatingDoTaskFluent< SELF extends DelegatingDoTaskFluent, LIST extends BaseTaskItemListBuilder> - extends DoTaskFluent { + extends DoTaskFluent, HasDelegate { - /** The underlying DoTaskFluent to forward to. */ - DoTaskFluent internalDelegate(); - - /** Convenience cast; implementors just return (SELF) this in practice. */ @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) { - internalDelegate().set(name, cfg); + d().set(name, cfg); return self(); } @Override default SELF set(Consumer cfg) { - internalDelegate().set(cfg); + d().set(cfg); return self(); } @Override default SELF set(String name, String expr) { - internalDelegate().set(name, expr); + d().set(name, expr); return self(); } @Override default SELF set(String expr) { - internalDelegate().set(expr); + d().set(expr); return self(); } @Override default SELF forEach(String name, Consumer> cfg) { - internalDelegate().forEach(name, cfg); + d().forEach(name, cfg); return self(); } @Override default SELF forEach(Consumer> cfg) { - internalDelegate().forEach(cfg); + d().forEach(cfg); return self(); } @Override default SELF switchCase(String name, Consumer cfg) { - internalDelegate().switchCase(name, cfg); + d().switchCase(name, cfg); return self(); } @Override default SELF switchCase(Consumer cfg) { - internalDelegate().switchCase(cfg); + d().switchCase(cfg); return self(); } @Override default SELF raise(String name, Consumer cfg) { - internalDelegate().raise(name, cfg); + d().raise(name, cfg); return self(); } @Override default SELF raise(Consumer cfg) { - internalDelegate().raise(cfg); + d().raise(cfg); return self(); } @Override default SELF fork(String name, Consumer cfg) { - internalDelegate().fork(name, cfg); + d().fork(name, cfg); return self(); } @Override default SELF fork(Consumer cfg) { - internalDelegate().fork(cfg); + d().fork(cfg); return self(); } @Override default SELF listen(String name, Consumer cfg) { - internalDelegate().listen(name, cfg); + d().listen(name, cfg); return self(); } @Override default SELF listen(Consumer cfg) { - internalDelegate().listen(cfg); + d().listen(cfg); return self(); } @Override default SELF emit(String name, Consumer cfg) { - internalDelegate().emit(name, cfg); + d().emit(name, cfg); return self(); } @Override default SELF emit(Consumer cfg) { - internalDelegate().emit(cfg); + d().emit(cfg); return self(); } @Override default SELF tryCatch(String name, Consumer> cfg) { - internalDelegate().tryCatch(name, cfg); + d().tryCatch(name, cfg); return self(); } @Override default SELF tryCatch(Consumer> cfg) { - internalDelegate().tryCatch(cfg); + d().tryCatch(cfg); return self(); } @Override default SELF callHTTP(String name, Consumer cfg) { - internalDelegate().callHTTP(name, cfg); + d().callHTTP(name, cfg); return self(); } @Override default SELF callHTTP(Consumer cfg) { - internalDelegate().callHTTP(cfg); + d().callHTTP(cfg); return self(); } } 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(); +} From 2015576cd7579df2bd2608091fbc942b079897d1 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 23 Jul 2025 16:04:33 -0400 Subject: [PATCH 3/5] Add sequence Signed-off-by: Ricardo Zanini --- fluent/agentic/pom.xml | 4 ++ .../fluent/agentic/AgentDoTaskFluent.java | 12 +++++ .../agentic/AgentTaskItemListBuilder.java | 8 ++++ .../agentic/DelegatingAgentDoTaskFluent.java | 8 ++++ .../agentic/AgenticWorkflowBuilderTest.java | 47 +++++++++++++++++++ 5 files changed, 79 insertions(+) diff --git a/fluent/agentic/pom.xml b/fluent/agentic/pom.xml index 49d0e67c..74869509 100644 --- a/fluent/agentic/pom.xml +++ b/fluent/agentic/pom.xml @@ -58,6 +58,10 @@ mockito-core test + + org.assertj + assertj-core + dev.langchain4j langchain4j-ollama 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 index bd349a82..2c5603f1 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java @@ -15,7 +15,19 @@ */ package io.serverlessworkflow.fluent.agentic; +import java.util.UUID; + 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); + } } 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 index 2d86faf7..03eb016e 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -42,6 +42,14 @@ public AgentTaskItemListBuilder agent(String name, Object agent) { return this; } + @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 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 index 4fc23f14..f94d6dda 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -27,8 +27,16 @@ private AgentDoTaskFluent d() { } @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; + } } 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 index 755d9f3a..ba6566f4 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.agentic; 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; @@ -24,6 +25,7 @@ import dev.langchain4j.agentic.AgentServices; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.api.types.func.CallJava; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; public class AgenticWorkflowBuilderTest { @@ -46,4 +48,49 @@ public void verifyAgentCall() { 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 + @DisplayName("Mix spec verbs with agent()") + 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(); + } + + private Agents.MovieExpert newMovieExpert() { + return spy( + AgentServices.agentBuilder(Agents.MovieExpert.class) + .outputName("movies") + .chatModel(BASE_MODEL) + .build()); + } } From 83deef19e6df7f4e236faabfea5710920c7b197e Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 23 Jul 2025 18:20:53 -0400 Subject: [PATCH 4/5] Add loop DSL Signed-off-by: Ricardo Zanini --- .../fluent/agentic/AgentAdapters.java | 6 ++ .../fluent/agentic/AgentDoTaskFluent.java | 7 ++ .../agentic/AgentTaskItemListBuilder.java | 11 +++ .../agentic/DelegatingAgentDoTaskFluent.java | 7 ++ .../fluent/agentic/LoopAgentsBuilder.java | 72 ++++++++++++++++ .../agentic/AgentTaskItemListBuilderTest.java | 86 +++++++++++++++++++ .../agentic/AgenticWorkflowBuilderTest.java | 65 ++++++++++++-- .../fluent/agentic/Agents.java | 10 +-- .../fluent/agentic/AgentsUtils.java | 34 ++++++++ 9 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java 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 index b65c9a12..eafd2a82 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java @@ -20,8 +20,10 @@ 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 { @@ -34,4 +36,8 @@ public static List toExecutors(Object... agents) { 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/AgentDoTaskFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java index 2c5603f1..1828f204 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java @@ -16,6 +16,7 @@ package io.serverlessworkflow.fluent.agentic; import java.util.UUID; +import java.util.function.Consumer; public interface AgentDoTaskFluent> { @@ -30,4 +31,10 @@ default SELF agent(Object agent) { 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); + } } 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 index 03eb016e..36099fca 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -15,8 +15,11 @@ */ package io.serverlessworkflow.fluent.agentic; +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.function.Consumer; public class AgentTaskItemListBuilder extends BaseTaskItemListBuilder implements AgentDoTaskFluent { @@ -50,6 +53,14 @@ public AgentTaskItemListBuilder sequence(String name, Object... agents) { 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 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 index f94d6dda..7758f8e4 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -17,6 +17,7 @@ import io.serverlessworkflow.fluent.func.DelegatingFuncDoTaskFluent; import io.serverlessworkflow.fluent.spec.HasDelegate; +import java.util.function.Consumer; public interface DelegatingAgentDoTaskFluent> extends AgentDoTaskFluent, DelegatingFuncDoTaskFluent, HasDelegate { @@ -39,4 +40,10 @@ default SELF sequence(String name, Object... agents) { d().sequence(name, agents); return (SELF) this; } + + @SuppressWarnings("unchecked") + default SELF loop(String name, Consumer consumer) { + d().loop(name, consumer); + 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..90403c9a --- /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 tasks + items.forEach(it -> assertThat(it.getTask().getCallTask().get()).isNotNull()); + } + + @Test + @DisplayName("loop(name, builder) produces a ForTaskFunction with inner call tasks") + 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 index ba6566f4..510d9725 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -15,6 +15,7 @@ */ 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; @@ -23,9 +24,14 @@ import static org.mockito.Mockito.spy; import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.Cognisphere; +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 org.junit.jupiter.api.DisplayName; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import org.junit.jupiter.api.Test; public class AgenticWorkflowBuilderTest { @@ -69,7 +75,6 @@ void sequenceAgents() { } @Test - @DisplayName("Mix spec verbs with agent()") void mixSpecAndAgent() { Workflow wf = AgenticWorkflowBuilder.workflow("mixFlow") @@ -86,11 +91,55 @@ void mixSpecAndAgent() { assertThat(wf.getDo().get(2).getTask().getSetTask()).isNotNull(); } - private Agents.MovieExpert newMovieExpert() { - return spy( - AgentServices.agentBuilder(Agents.MovieExpert.class) - .outputName("movies") - .chatModel(BASE_MODEL) - .build()); + @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); } } 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 298b8009..81f71e4c 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 @@ -26,11 +26,11 @@ 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. - """) + 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()); + } +} From d596acb455f434b200e19cb29d258d30578ffc64 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 23 Jul 2025 19:09:20 -0400 Subject: [PATCH 5/5] Added forkFn support and parallel Signed-off-by: Ricardo Zanini --- .../fluent/agentic/AgentDoTaskFluent.java | 6 ++ .../agentic/AgentTaskItemListBuilder.java | 18 ++++- .../agentic/DelegatingAgentDoTaskFluent.java | 8 ++ .../agentic/AgentTaskItemListBuilderTest.java | 4 +- .../agentic/AgenticWorkflowBuilderTest.java | 30 ++++++++ .../func/DelegatingFuncDoTaskFluent.java | 12 +++ .../fluent/func/FuncDoTaskFluent.java | 4 + .../fluent/func/FuncForkTaskBuilder.java | 74 +++++++++++++++++++ .../fluent/func/FuncTaskItemListBuilder.java | 14 ++++ .../fluent/func/JavaWorkflowBuilderTest.java | 4 +- 10 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java 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 index 1828f204..59055322 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskFluent.java @@ -37,4 +37,10 @@ default SELF sequence(Object... agents) { 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 index 36099fca..6f1012fe 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -15,10 +15,12 @@ */ 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 @@ -42,7 +44,7 @@ public AgentTaskItemListBuilder agent(String name, Object agent) { .forEach( exec -> this.funcDelegate.callFn(name, fn -> fn.function(AgentAdapters.toFunction(exec)))); - return this; + return self(); } @Override @@ -61,6 +63,20 @@ public AgentTaskItemListBuilder loop(String name, Consumer co 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/DelegatingAgentDoTaskFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java index 7758f8e4..7f89ca0f 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/DelegatingAgentDoTaskFluent.java @@ -42,8 +42,16 @@ default SELF sequence(String name, Object... agents) { } @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/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java index 90403c9a..8b9585db 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java @@ -61,12 +61,12 @@ void testSequence() { assertThat(items.get(1).getName()).isEqualTo("seq-1"); assertThat(items.get(2).getName()).isEqualTo("seq-2"); - // All must be call tasks + // 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 tasks") + @DisplayName("loop(name, builder) produces a ForTaskFunction with inner call branche") void testLoop() { AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); Agents.MovieExpert scorer = AgentsUtils.newMovieExpert(); 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 index 510d9725..51515654 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowBuilderTest.java @@ -25,6 +25,7 @@ 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; @@ -32,6 +33,7 @@ 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 { @@ -142,4 +144,32 @@ void loopWithMaxIterationsAndExitCondition() { 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/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java index edb8375d..131330fa 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/DelegatingFuncDoTaskFluent.java @@ -71,4 +71,16 @@ 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/FuncDoTaskFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java index ae65a478..9ec0cb39 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncDoTaskFluent.java @@ -30,4 +30,8 @@ public interface FuncDoTaskFluent> { 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 9bc40f9a..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 @@ -82,4 +82,18 @@ 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/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java b/fluent/func/src/test/java/io/serverlessworkflow/fluent/func/JavaWorkflowBuilderTest.java index 3aa1e092..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 @@ -165,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(); @@ -219,7 +219,7 @@ void testCallJavaTask() { } @Test - @DisplayName("switchCaseFn (Java variant) coexists with spec tasks") + @DisplayName("switchCaseFn (Java variant) coexists with spec branche") void testSwitchCaseJava() { Workflow wf = FuncWorkflowBuilder.workflow("switchJava")