diff --git a/experimental/agentic/pom.xml b/experimental/agentic/pom.xml index dec8a5e5..855838c5 100644 --- a/experimental/agentic/pom.xml +++ b/experimental/agentic/pom.xml @@ -7,7 +7,7 @@ 8.0.0-SNAPSHOT serverlessworkflow-experimental-agentic - ServelessWorkflow:: Experimental:: Agentic + Serverless Workflow :: Experimental :: Agentic io.serverlessworkflow diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java index 3d352812..ef1f0a55 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java @@ -24,31 +24,33 @@ class AgenticModel extends JavaModel { - AgenticModel(AgenticScope agenticScope) { - super(agenticScope); + private final AgenticScope agenticScope; + + AgenticModel(AgenticScope agenticScope, Object object) { + super(object); + this.agenticScope = agenticScope; } - @Override - public void setObject(Object obj) { - super.setObject(obj); + public AgenticScope getAgenticScope() { + return agenticScope; } @Override public Collection asCollection() { - throw new UnsupportedOperationException("Not supported yet."); + throw new UnsupportedOperationException("asCollection() is not supported yet."); } @Override public Optional> asMap() { - return Optional.of(((AgenticScope) object).state()); + return Optional.of(this.agenticScope.state()); } @Override public Optional as(Class clazz) { if (AgenticScope.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(object)); + return Optional.of(clazz.cast(this.agenticScope)); } else if (Map.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(((AgenticScope) object).state())); + return asMap().map(clazz::cast); } else { return super.as(clazz); } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java index 2ea0e382..90ef8e73 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java @@ -19,25 +19,37 @@ import dev.langchain4j.agentic.scope.ResultWithAgenticScope; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.expressions.func.JavaModelCollection; -import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.Optional; -class AgenticModelCollection extends JavaModelCollection { +public class AgenticModelCollection extends JavaModelCollection { private final AgenticScope agenticScope; + private final AgenticScopeCloudEventsHandler ceHandler; - AgenticModelCollection(Collection object, AgenticScope agenticScope) { - super(object); - this.agenticScope = agenticScope; - } - - AgenticModelCollection(AgenticScope agenticScope) { + AgenticModelCollection(AgenticScope agenticScope, AgenticScopeCloudEventsHandler ceHandler) { + super(Collections.emptyList()); this.agenticScope = agenticScope; + this.ceHandler = ceHandler; } @Override - protected WorkflowModel nextItem(Object obj) { - return new AgenticModel((AgenticScope) obj); + public boolean add(WorkflowModel e) { + Optional> asMap = e.asMap(); + if (asMap.isPresent() && !asMap.get().isEmpty()) { + this.agenticScope.writeStates(asMap.get()); + return super.add(e); + } + + // Update the agenticScope with the event body, so agents can use the event data as input + Object value = e.asJavaObject(); + if (!ceHandler.writeStateIfCloudEvent(this.agenticScope, value)) { + this.agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + } + + // add to the collection + return super.add(e); } @Override @@ -46,6 +58,8 @@ public Optional as(Class clazz) { return Optional.of(clazz.cast(agenticScope)); } else if (ResultWithAgenticScope.class.isAssignableFrom(clazz)) { return Optional.of(clazz.cast(new ResultWithAgenticScope<>(agenticScope, object))); + } else if (Map.class.isAssignableFrom(clazz)) { + return Optional.of(clazz.cast(agenticScope.state())); } else { return super.as(clazz); } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java index 9f95c0d5..bed5dc9f 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java @@ -22,25 +22,40 @@ import io.serverlessworkflow.impl.WorkflowModelCollection; import io.serverlessworkflow.impl.WorkflowModelFactory; import io.serverlessworkflow.impl.expressions.agentic.langchain4j.AgenticScopeRegistryAssessor; -import io.serverlessworkflow.impl.expressions.func.JavaModel; import java.time.OffsetDateTime; import java.util.Map; class AgenticModelFactory implements WorkflowModelFactory { - /** - * Applies any change to the model after running as task. We will always set it to a @AgenticScope - * object since @AgentExecutor is always adding the output to the agenticScope. We just have to - * make sure that agenticScope is always passed to the next input task. - * - * @param prev the global AgenticScope object getting updated by the workflow context - * @param obj the same AgenticScope object updated by the AgentExecutor - * @return the workflow context model holding the agenticScope object. - */ + static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; + private final AgenticScopeRegistryAssessor scopeRegistryAssessor = + new AgenticScopeRegistryAssessor(); + private final AgenticScopeCloudEventsHandler scopeCloudEventsHandler = + new AgenticScopeCloudEventsHandler(); + + @SuppressWarnings("unchecked") + private AgenticModel newAgenticModel(Object state) { + if (state == null) { + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), null); + } + + if (state instanceof Map) { + this.scopeRegistryAssessor.writeStates((Map) state); + } else { + this.scopeRegistryAssessor.writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, state); + } + + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), state); + } + @Override public WorkflowModel fromAny(WorkflowModel prev, Object obj) { - // We ignore `obj` since it's already included in `prev` within the agenticScope instance - return prev; + // TODO: we shouldn't update the state if the previous task was an agent call since under the + // hood, the agent already updated it. + if (prev instanceof AgenticModel agenticModel) { + this.scopeRegistryAssessor.setAgenticScope(agenticModel.getAgenticScope()); + } + return newAgenticModel(obj); } @Override @@ -53,58 +68,55 @@ public WorkflowModel combine(Map workflowVariables) { @Override public WorkflowModelCollection createCollection() { - throw new UnsupportedOperationException(); + return new AgenticModelCollection( + this.scopeRegistryAssessor.getAgenticScope(), scopeCloudEventsHandler); } - // TODO: all these methods can use agenticScope as long as we have access to the `outputName` - @Override public WorkflowModel from(boolean value) { - return new JavaModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(Number value) { - return new JavaModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(String value) { - return new JavaModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(CloudEvent ce) { - return new JavaModel(ce); + return from(scopeCloudEventsHandler.extractDataAsMap(ce)); } @Override public WorkflowModel from(CloudEventData ce) { - return new JavaModel(ce); + return from(scopeCloudEventsHandler.extractDataAsMap(ce)); } @Override public WorkflowModel from(OffsetDateTime value) { - return new JavaModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(Map map) { - final AgenticScope agenticScope = new AgenticScopeRegistryAssessor().getAgenticScope(); - agenticScope.writeStates(map); - return new AgenticModel(agenticScope); + return newAgenticModel(map); } @Override public WorkflowModel fromNull() { - return new JavaModel(null); + return newAgenticModel(null); } @Override public WorkflowModel fromOther(Object value) { - if (value instanceof AgenticScope) { - return new AgenticModel((AgenticScope) value); + if (value instanceof AgenticScope scope) { + return new AgenticModel(scope, scope.state()); } - return new JavaModel(value); + return newAgenticModel(value); } } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticScopeCloudEventsHandler.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticScopeCloudEventsHandler.java new file mode 100644 index 00000000..8e9347eb --- /dev/null +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticScopeCloudEventsHandler.java @@ -0,0 +1,70 @@ +/* + * 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.impl.expressions.agentic; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.agentic.scope.AgenticScope; +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; +import java.io.IOException; +import java.util.Map; + +public final class AgenticScopeCloudEventsHandler { + + private final ObjectMapper mapper = new ObjectMapper(); + + AgenticScopeCloudEventsHandler() {} + + public void writeState(final AgenticScope scope, final CloudEvent cloudEvent) { + if (cloudEvent != null) { + writeState(scope, cloudEvent.getData()); + } + } + + public void writeState(final AgenticScope scope, final CloudEventData cloudEvent) { + scope.writeStates(extractDataAsMap(cloudEvent)); + } + + public boolean writeStateIfCloudEvent(final AgenticScope scope, final Object value) { + if (value instanceof CloudEvent) { + writeState(scope, (CloudEvent) value); + return true; + } else if (value instanceof CloudEventData) { + writeState(scope, (CloudEventData) value); + return true; + } + return false; + } + + public Map extractDataAsMap(final CloudEventData ce) { + try { + if (ce != null) { + return mapper.readValue(ce.toBytes(), new TypeReference<>() {}); + } + } catch (IOException e) { + throw new IllegalArgumentException("Unable to parse CloudEvent data as JSON", e); + } + return Map.of(); + } + + public Map extractDataAsMap(final CloudEvent ce) { + if (ce != null) { + return extractDataAsMap(ce.getData()); + } + return Map.of(); + } +} diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java index 01ccd1cd..1d3b5ab1 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java @@ -16,8 +16,10 @@ package io.serverlessworkflow.impl.expressions.agentic.langchain4j; import dev.langchain4j.agentic.internal.AgenticScopeOwner; +import dev.langchain4j.agentic.scope.AgenticScope; import dev.langchain4j.agentic.scope.AgenticScopeRegistry; import dev.langchain4j.agentic.scope.DefaultAgenticScope; +import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -27,7 +29,7 @@ public class AgenticScopeRegistryAssessor implements AgenticScopeOwner { private final AtomicReference agenticScopeRegistry = new AtomicReference<>(); private final String agentId; - private DefaultAgenticScope agenticScope; + private AgenticScope agenticScope; private Object memoryId; public AgenticScopeRegistryAssessor(String agentId) { @@ -44,7 +46,7 @@ public void setMemoryId(Object memoryId) { this.memoryId = memoryId; } - public DefaultAgenticScope getAgenticScope() { + public AgenticScope getAgenticScope() { if (agenticScope != null) { return agenticScope; } @@ -57,9 +59,21 @@ public DefaultAgenticScope getAgenticScope() { return this.agenticScope; } + public void setAgenticScope(AgenticScope agenticScope) { + this.agenticScope = Objects.requireNonNull(agenticScope, "AgenticScope cannot be null"); + } + + public void writeState(String key, Object value) { + this.getAgenticScope().writeState(key, value); + } + + public void writeStates(Map states) { + this.getAgenticScope().writeStates(states); + } + @Override public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { - this.agenticScope = agenticScope; + this.setAgenticScope(agenticScope); return this; } diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java index 897b8d5c..e1d4dae3 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java @@ -65,6 +65,7 @@ public Optional asNumber() { @Override public Optional> asMap() { + return object instanceof Map ? Optional.of((Map) object) : Optional.empty(); } diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelCollection.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelCollection.java index f4ac21a7..2f84411a 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelCollection.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelCollection.java @@ -46,7 +46,7 @@ public boolean isEmpty() { @Override public boolean contains(Object o) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("contains() is not supported yet"); } private class ModelIterator implements Iterator { @@ -80,12 +80,12 @@ public Iterator iterator() { @Override public Object[] toArray() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("toArray is not supported yet"); } @Override public T[] toArray(T[] a) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("toArray is not supported yet"); } @Override @@ -100,7 +100,7 @@ public boolean remove(Object o) { @Override public boolean containsAll(Collection c) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("containsAll is not supported yet"); } @Override @@ -119,7 +119,7 @@ public boolean removeAll(Collection c) { @Override public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("retainAll() is not supported yet"); } @Override diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelFactory.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelFactory.java index c314bea7..4502abf1 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelFactory.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModelFactory.java @@ -23,7 +23,7 @@ import java.time.OffsetDateTime; import java.util.Map; -class JavaModelFactory implements WorkflowModelFactory { +public class JavaModelFactory implements WorkflowModelFactory { private final JavaModel TrueModel = new JavaModel(Boolean.TRUE); private final JavaModel FalseModel = new JavaModel(Boolean.FALSE); private final JavaModel NullModel = new JavaModel(null); diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java index 0f7ce656..5ffbfb3a 100644 --- a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java @@ -115,20 +115,20 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } // invoke - return executeWorkflow(currentCognisphere(method, args), method, args); + return executeWorkflow(currentAgenticScope(method, args), method, args); } - private Object executeWorkflow(DefaultAgenticScope agenticScope, Method method, Object[] args) { + private Object executeWorkflow(AgenticScope agenticScope, Method method, Object[] args) { writeAgenticScopeState(agenticScope, method, args); try (WorkflowApplication app = workflowApplicationBuilder.build()) { // TODO improve result handling - DefaultAgenticScope output = + AgenticScope output = app.workflowDefinition(workflow) .instance(agenticScope) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow( () -> new IllegalArgumentException( @@ -149,7 +149,7 @@ private Object executeWorkflow(DefaultAgenticScope agenticScope, Method method, } } - private DefaultAgenticScope currentCognisphere(Method method, Object[] args) { + private AgenticScope currentAgenticScope(Method method, Object[] args) { Object memoryId = memoryId(method, args); this.agenticScopeRegistryAssessor.setMemoryId(memoryId); return this.agenticScopeRegistryAssessor.getAgenticScope(); 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 526deac0..5d7861d8 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 @@ -81,6 +81,12 @@ public AgentDoTaskBuilder emit(String name, Consumer itemsC return self(); } + @Override + public AgentDoTaskBuilder listen(String name, Consumer itemsConfigurer) { + this.listBuilder().listen(name, itemsConfigurer); + return self(); + } + @Override public AgentDoTaskBuilder forEach(String name, Consumer itemsConfigurer) { this.listBuilder().forEach(name, itemsConfigurer); diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java new file mode 100644 index 00000000..5a9b9359 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.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 io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; +import io.serverlessworkflow.api.types.ListenTask; +import io.serverlessworkflow.api.types.func.UntilPredicate; +import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; +import java.util.function.Predicate; + +public class AgentListenTaskBuilder extends ListenTaskBuilder { + + private UntilPredicate untilPredicate; + + public AgentListenTaskBuilder() { + super(new AgentTaskItemListBuilder()); + } + + public AgentListenTaskBuilder until(Predicate predicate, Class predClass) { + untilPredicate = new UntilPredicate().withPredicate(predicate, predClass); + return this; + } + + @Override + public ListenTask build() { + ListenTask task = super.build(); + AnyEventConsumptionStrategy anyEvent = + task.getListen().getTo().getAnyEventConsumptionStrategy(); + if (untilPredicate != null && anyEvent != null) { + anyEvent.withUntil(untilPredicate); + } + return task; + } +} 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 a26f0b45..528953c5 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 @@ -113,6 +113,15 @@ public AgentTaskItemListBuilder emit(String name, Consumer return self(); } + @Override + public AgentTaskItemListBuilder listen( + String name, Consumer itemsConfigurer) { + final AgentListenTaskBuilder builder = new AgentListenTaskBuilder(); + itemsConfigurer.accept(builder); + this.addTaskItem(new TaskItem(name, new Task().withListenTask(builder.build()))); + return self(); + } + @Override public AgentTaskItemListBuilder forEach( String name, Consumer itemsConfigurer) { diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java index aaa7176f..1b0f05dd 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java @@ -15,12 +15,32 @@ */ package io.serverlessworkflow.fluent.agentic.spi; +import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; import io.serverlessworkflow.fluent.agentic.LoopAgentsBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; +import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncForTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncForkTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncSetTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; +import io.serverlessworkflow.fluent.func.spi.CallFnFluent; +import io.serverlessworkflow.fluent.spec.spi.EmitFluent; +import io.serverlessworkflow.fluent.spec.spi.ForEachFluent; +import io.serverlessworkflow.fluent.spec.spi.ForkFluent; +import io.serverlessworkflow.fluent.spec.spi.ListenFluent; +import io.serverlessworkflow.fluent.spec.spi.SetFluent; +import io.serverlessworkflow.fluent.spec.spi.SwitchFluent; import java.util.UUID; import java.util.function.Consumer; -public interface AgentDoFluent> extends FuncDoFluent { +public interface AgentDoFluent> + extends SetFluent, + EmitFluent, + ForEachFluent, + SwitchFluent, + ForkFluent, + ListenFluent, + CallFnFluent { SELF agent(String name, Object agent); 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 a0c970ab..e8b53006 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 @@ -23,13 +23,23 @@ public interface Agents { + interface ChatBot { + @UserMessage( + """ + You are a happy chat bot, reply to my message: + {{userInput}}. + """) + @Agent + String chat(@V("userInput") String userInput); + } + interface MovieExpert { @UserMessage( """ You are a great evening planner. Propose a list of 3 movies matching the given mood. - The mood is {mood}. + The mood is {{mood}}. Provide a list with the 3 items and nothing else. """) @Agent diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java new file mode 100644 index 00000000..397f2183 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -0,0 +1,242 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.scope.AgenticScope; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.v1.CloudEventBuilder; +import io.serverlessworkflow.api.types.EventFilter; +import io.serverlessworkflow.api.types.EventProperties; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowInstance; +import io.serverlessworkflow.impl.WorkflowModel; +import io.serverlessworkflow.impl.WorkflowStatus; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class ChatBotIT { + + @Test + @SuppressWarnings("unchecked") + @Disabled("Figuring out event processing") + void chat_bot() { + Agents.ChatBot chatBot = + spy( + AgenticServices.agentBuilder(Agents.ChatBot.class) + .chatModel(Models.BASE_MODEL) + // .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) + .outputName("conversation") + .build()); + BlockingQueue replyEvents = new LinkedBlockingQueue<>(); + BlockingQueue finishedEvents = new LinkedBlockingQueue<>(); + + // 1. listen to an event containing `message` key in the body + // 2. if contains, call the agent, if not end the workflow + // 3. After replying to the chat, return + final Workflow listenWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.listen( + l -> + l.to( + to -> + to.any( + c -> + c.with( + event -> + event.type( + "org.acme.chatbot.request"))) + .until( + until -> + until.one( + one -> + one.with( + e -> + e.type( + "org.acme.chatbot.finalize"))))) + .forEach( + f -> + f.tasks( + tasks -> + tasks + .agent(chatBot) + .emit( + emit -> + emit.event( + e -> + e.type( + "org.acme.chatbot.reply")))))) + .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.finished")))) + .build(); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + app.eventConsumer() + .register( + app.eventConsumer() + .listen( + new EventFilter() + .withWith(new EventProperties().withType("org.acme.chatbot.reply")), + app), + ce -> replyEvents.add((CloudEvent) ce)); + + app.eventConsumer() + .register( + app.eventConsumer() + .listen( + new EventFilter() + .withWith(new EventProperties().withType("org.acme.chatbot.finished")), + app), + ce -> finishedEvents.add((CloudEvent) ce)); + + final WorkflowInstance waitingInstance = + app.workflowDefinition(listenWorkflow).instance(Map.of()); + final CompletableFuture runningModel = waitingInstance.start(); + + // The workflow is just waiting for the event + assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); + + // Publish the events + app.eventPublisher().publish(newMessageEvent("Hello World!")); + CloudEvent reply = replyEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(reply); + + // Empty message completes the workflow + app.eventPublisher().publish(newMessageEvent("", "org.acme.chatbot.finalize")); + CloudEvent finished = finishedEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(finished); + assertThat(finishedEvents).isEmpty(); + + assertThat(runningModel).isCompleted(); + assertEquals(WorkflowStatus.COMPLETED, waitingInstance.status()); + + } catch (InterruptedException e) { + fail(e.getMessage()); + } + } + + /** + * In this test we validate a workflow mixed with agents and regular Java calls + * + *

+ * + *

    + *
  1. The first function prints the message input and converts the data into a Map for the + * agent ingestion + *
  2. Internally, our factories will add the output to a new AgenticScope since under the hood, + * we are call `as(AgenticScope)` + *
  3. The agent is then called with a scope with a state as `message="input"` + *
  4. The agent updates the state automatically in the AgenticScope and returns the message as + * a string, this string is then served to the next task + *
  5. The next task process the agent response and returns it ending the workflow. Meanwhile, + * the AgenticScope is always updated with the latest result from the given task. + *
+ */ + @Test + void mixed_workflow() { + Agents.ChatBot chatBot = + spy( + AgenticServices.agentBuilder(Agents.ChatBot.class) + .chatModel(Models.BASE_MODEL) + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) + .outputName("userInput") + .build()); + + final Workflow mixedWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.callFn( + callJ -> + callJ.function( + input -> { + System.out.println(input); + return Map.of("userInput", input); + }, + String.class)) + .agent(chatBot) + .callFn( + callJ -> + callJ.function( + input -> { + System.out.println(input); + // Here, we are return a simple string so the internal + // AgenticScope will add it to the default `input` key + // If we want to really manipulate it, we could return a + // Map<>(message, input) + return "I've changed the input [" + input + "]"; + }, + String.class))) + .build(); + + try (WorkflowApplication app = WorkflowApplication.builder().build()) { + WorkflowModel model = + app.workflowDefinition(mixedWorkflow).instance("Hello World!").start().join(); + + Optional resultAsString = model.as(String.class); + + assertTrue(resultAsString.isPresent()); + assertFalse(resultAsString.get().isEmpty()); + assertTrue(resultAsString.get().contains("changed the input")); + + Optional resultAsScope = model.as(AgenticScope.class); + + assertTrue(resultAsScope.isPresent()); + assertFalse(resultAsScope.get().readState("input").toString().isEmpty()); + assertTrue(resultAsScope.get().readState("input").toString().contains("changed the input")); + } + } + + private CloudEvent newMessageEvent(String message) { + return newMessageEvent(message, null); + } + + private CloudEvent newMessageEvent(String message, String type) { + if (type == null || type.isEmpty()) { + type = "org.acme.chatbot.request"; + } + + return new CloudEventBuilder() + .withData(String.format("{\"userInput\": \"%s\"}", message).getBytes()) + .withType(type) + .withId(UUID.randomUUID().toString()) + .withDataContentType("application/json") + .withSource(URI.create("test://localhost")) + .withSubject("A chatbot message") + .withTime(OffsetDateTime.now()) + .build(); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java index e06aafda..170281c1 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java @@ -22,7 +22,7 @@ public class Models { static final ChatModel BASE_MODEL = OllamaChatModel.builder() - .baseUrl("http://127.0.0.1:1143") + .baseUrl("http://127.0.0.1:11434") .modelName("qwen2.5:7b") .timeout(Duration.ofMinutes(10)) .temperature(0.0) diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java index acf5c411..d38e863c 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java @@ -23,7 +23,7 @@ import static org.mockito.Mockito.when; import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; +import dev.langchain4j.agentic.scope.AgenticScope; import dev.langchain4j.agentic.workflow.HumanInTheLoop; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.impl.WorkflowApplication; @@ -53,12 +53,12 @@ public void testAgent() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(topic) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals("storySeedAgent", result.readState("premise")); @@ -93,12 +93,12 @@ public void testAgents() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(topic) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals("sceneAgent", result.readState("story")); @@ -129,12 +129,12 @@ public void testSequence() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(topic) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals("sceneAgent", result.readState("story")); @@ -166,12 +166,12 @@ public void testParallel() throws ExecutionException, InterruptedException { topic.put("style", "sci-fi"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(topic) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals("Fake conflict response", result.readState("setting")); @@ -212,12 +212,12 @@ public void testSeqAndThenParallel() throws ExecutionException, InterruptedExcep topic.put("fact", "alien"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(topic) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals(cultureTraits, result.readState("culture")); @@ -274,12 +274,12 @@ public void humanInTheLoop() throws ExecutionException, InterruptedException { initialValues.put("agenda", "Discuss project updates"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - DefaultAgenticScope result = + AgenticScope result = app.workflowDefinition(workflow) .instance(initialValues) .start() .get() - .as(DefaultAgenticScope.class) + .as(AgenticScope.class) .orElseThrow(); assertEquals("Styled meeting invitation for John Doe", result.readState("styled")); 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 723e8d23..613f76a2 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 @@ -41,6 +41,12 @@ public FuncDoTaskBuilder emit(String name, Consumer itemsCo return this; } + @Override + public FuncDoTaskBuilder listen(String name, Consumer itemsConfigurer) { + this.listBuilder().listen(name, itemsConfigurer); + return this; + } + @Override public FuncDoTaskBuilder forEach(String name, Consumer itemsConfigurer) { this.listBuilder().forEach(name, itemsConfigurer); diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java index 89325ee8..28c8b8a4 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncEmitTaskBuilder.java @@ -16,10 +16,12 @@ package io.serverlessworkflow.fluent.func; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; +import io.serverlessworkflow.fluent.func.spi.FuncTransformations; import io.serverlessworkflow.fluent.spec.EmitTaskBuilder; public class FuncEmitTaskBuilder extends EmitTaskBuilder - implements ConditionalTaskBuilder { + implements ConditionalTaskBuilder, + FuncTransformations { FuncEmitTaskBuilder() { super(); } diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java new file mode 100644 index 00000000..8b5a57fc --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.func; + +import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; +import io.serverlessworkflow.api.types.ListenTask; +import io.serverlessworkflow.api.types.func.UntilPredicate; +import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; +import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; +import java.util.function.Predicate; + +public class FuncListenTaskBuilder extends ListenTaskBuilder + implements ConditionalTaskBuilder, + FuncTransformations { + + private UntilPredicate untilPredicate; + + FuncListenTaskBuilder() { + super(new FuncTaskItemListBuilder()); + } + + public FuncListenTaskBuilder until(Predicate predicate, Class predClass) { + untilPredicate = new UntilPredicate().withPredicate(predicate, predClass); + return this; + } + + @Override + public ListenTask build() { + ListenTask task = super.build(); + AnyEventConsumptionStrategy anyEvent = + task.getListen().getTo().getAnyEventConsumptionStrategy(); + if (untilPredicate != null && anyEvent != null) { + anyEvent.withUntil(untilPredicate); + } + return task; + } +} 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 2c8b5524..6ef8d7b0 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 @@ -79,6 +79,16 @@ public FuncTaskItemListBuilder emit(String name, Consumer i new TaskItem(name, new Task().withEmitTask(emitTaskJavaBuilder.build()))); } + @Override + public FuncTaskItemListBuilder listen( + String name, Consumer itemsConfigurer) { + this.requireNameAndConfig(name, itemsConfigurer); + final FuncListenTaskBuilder listenTaskJavaBuilder = new FuncListenTaskBuilder(); + itemsConfigurer.accept(listenTaskJavaBuilder); + return this.addTaskItem( + new TaskItem(name, new Task().withListenTask(listenTaskJavaBuilder.build()))); + } + @Override public FuncTaskItemListBuilder forEach( String name, Consumer itemsConfigurer) { diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/CallFnFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/CallFnFluent.java new file mode 100644 index 00000000..f576601b --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/CallFnFluent.java @@ -0,0 +1,29 @@ +/* + * 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.spi; + +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; +import java.util.UUID; +import java.util.function.Consumer; + +public interface CallFnFluent, LIST> { + + LIST callFn(String name, Consumer cfg); + + default LIST callFn(Consumer cfg) { + return this.callFn(UUID.randomUUID().toString(), cfg); + } +} diff --git a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java index 434304d5..9eebb194 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java @@ -19,15 +19,15 @@ import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; import io.serverlessworkflow.fluent.func.FuncForTaskBuilder; import io.serverlessworkflow.fluent.func.FuncForkTaskBuilder; +import io.serverlessworkflow.fluent.func.FuncListenTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSetTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; import io.serverlessworkflow.fluent.spec.spi.EmitFluent; import io.serverlessworkflow.fluent.spec.spi.ForEachFluent; import io.serverlessworkflow.fluent.spec.spi.ForkFluent; +import io.serverlessworkflow.fluent.spec.spi.ListenFluent; import io.serverlessworkflow.fluent.spec.spi.SetFluent; import io.serverlessworkflow.fluent.spec.spi.SwitchFluent; -import java.util.UUID; -import java.util.function.Consumer; // TODO: implement the other builders, e.g. CallHTTP @@ -36,11 +36,6 @@ public interface FuncDoFluent> EmitFluent, ForEachFluent, SwitchFluent, - ForkFluent { - - SELF callFn(String name, Consumer cfg); - - default SELF callFn(Consumer cfg) { - return this.callFn(UUID.randomUUID().toString(), cfg); - } -} + ForkFluent, + ListenFluent, + CallFnFluent {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java new file mode 100644 index 00000000..bdcadb79 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java @@ -0,0 +1,114 @@ +/* + * 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 io.serverlessworkflow.api.types.AllEventConsumptionStrategy; +import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; +import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; +import io.serverlessworkflow.api.types.Until; +import io.serverlessworkflow.fluent.spec.spi.EventConsumptionStrategyFluent; +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; + +public abstract class AbstractEventConsumptionStrategyBuilder< + SELF extends EventConsumptionStrategyFluent, T extends Serializable> + implements EventConsumptionStrategyFluent { + + protected boolean oneSet, allSet, anySet; + private Until until; + + AbstractEventConsumptionStrategyBuilder() {} + + @SuppressWarnings("unchecked") + private SELF self() { + return (SELF) this; + } + + public SELF one(Consumer c) { + ensureNoneSet(); + oneSet = true; + EventFilterBuilder fb = new EventFilterBuilder(); + c.accept(fb); + OneEventConsumptionStrategy strat = new OneEventConsumptionStrategy(); + strat.setOne(fb.build()); + this.setOne(strat); + return this.self(); + } + + abstract void setOne(OneEventConsumptionStrategy strategy); + + public SELF all(Consumer c) { + ensureNoneSet(); + allSet = true; + EventFilterBuilder fb = new EventFilterBuilder(); + c.accept(fb); + AllEventConsumptionStrategy strat = new AllEventConsumptionStrategy(); + strat.setAll(List.of(fb.build())); + this.setAll(strat); + return this.self(); + } + + abstract void setAll(AllEventConsumptionStrategy strategy); + + public SELF any(Consumer c) { + ensureNoneSet(); + anySet = true; + EventFilterBuilder fb = new EventFilterBuilder(); + c.accept(fb); + AnyEventConsumptionStrategy strat = new AnyEventConsumptionStrategy(); + strat.setAny(List.of(fb.build())); + this.setAny(strat); + return this.self(); + } + + abstract void setAny(AnyEventConsumptionStrategy strategy); + + public SELF until(Consumer c) { + final EventConsumptionStrategyBuilder eventConsumptionStrategyBuilder = + new EventConsumptionStrategyBuilder(); + c.accept(eventConsumptionStrategyBuilder); + this.until = new Until().withAnyEventUntilConsumed(eventConsumptionStrategyBuilder.build()); + return this.self(); + } + + public SELF until(String expression) { + this.until = new Until().withAnyEventUntilCondition(expression); + return this.self(); + } + + private void ensureNoneSet() { + if (oneSet || allSet || anySet) { + throw new IllegalStateException("Only one consumption strategy can be configured"); + } + } + + public final T build() { + if (!(oneSet || allSet || anySet)) { + throw new IllegalStateException( + "A consumption strategy (one, all, or any) must be configured"); + } + + if (anySet) { + this.setUntil(until); + } + return this.getEventConsumptionStrategy(); + } + + abstract T getEventConsumptionStrategy(); + + abstract void setUntil(Until until); +} 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 669b580f..4fc04278 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 @@ -56,7 +56,8 @@ public DoTaskBuilder fork(String name, Consumer itemsConfigurer } @Override - public DoTaskBuilder listen(String name, Consumer itemsConfigurer) { + public DoTaskBuilder listen( + String name, Consumer> itemsConfigurer) { this.listBuilder().listen(name, itemsConfigurer); return this; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java new file mode 100644 index 00000000..a681ca0e --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java @@ -0,0 +1,56 @@ +/* + * 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 io.serverlessworkflow.api.types.AllEventConsumptionStrategy; +import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; +import io.serverlessworkflow.api.types.EventConsumptionStrategy; +import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; +import io.serverlessworkflow.api.types.Until; + +public class EventConsumptionStrategyBuilder + extends AbstractEventConsumptionStrategyBuilder< + EventConsumptionStrategyBuilder, EventConsumptionStrategy> { + + private final EventConsumptionStrategy eventConsumptionStrategy = new EventConsumptionStrategy(); + + EventConsumptionStrategyBuilder() {} + + @Override + void setOne(OneEventConsumptionStrategy strategy) { + eventConsumptionStrategy.setOneEventConsumptionStrategy(strategy); + } + + @Override + void setAll(AllEventConsumptionStrategy strategy) { + eventConsumptionStrategy.setAllEventConsumptionStrategy(strategy); + } + + @Override + void setAny(AnyEventConsumptionStrategy strategy) { + eventConsumptionStrategy.setAnyEventConsumptionStrategy(strategy); + } + + @Override + EventConsumptionStrategy getEventConsumptionStrategy() { + return this.eventConsumptionStrategy; + } + + @Override + void setUntil(Until until) { + this.eventConsumptionStrategy.getAnyEventConsumptionStrategy().setUntil(until); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventFilterBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventFilterBuilder.java new file mode 100644 index 00000000..e99d524f --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventFilterBuilder.java @@ -0,0 +1,49 @@ +/* + * 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 io.serverlessworkflow.api.types.EventFilter; +import io.serverlessworkflow.api.types.EventFilterCorrelate; +import java.util.function.Consumer; + +/** Builder for event filters used in consumption strategies. */ +public final class EventFilterBuilder { + private final EventFilter filter = new EventFilter(); + private final EventFilterCorrelate correlate = new EventFilterCorrelate(); + + /** Predicate to match event properties. */ + public EventFilterBuilder with(Consumer c) { + EventPropertiesBuilder pb = new EventPropertiesBuilder(); + c.accept(pb); + filter.setWith(pb.build()); + return this; + } + + /** Correlation property for the filter. */ + public EventFilterBuilder correlate( + String key, Consumer c) { + ListenTaskBuilder.CorrelatePropertyBuilder cpb = + new ListenTaskBuilder.CorrelatePropertyBuilder(); + c.accept(cpb); + correlate.withAdditionalProperty(key, cpb.build()); + return this; + } + + public EventFilter build() { + filter.setCorrelate(correlate); + return filter; + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java new file mode 100644 index 00000000..a5f600c7 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java @@ -0,0 +1,66 @@ +/* + * 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 io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.ExportAs; +import io.serverlessworkflow.api.types.ExternalResource; +import io.serverlessworkflow.api.types.SchemaExternal; +import io.serverlessworkflow.api.types.SchemaInline; +import io.serverlessworkflow.api.types.SchemaUnion; + +public final class ExportBuilder { + private final Export export; + + public ExportBuilder() { + this.export = new Export(); + this.export.setAs(new ExportAs()); + this.export.setSchema(new SchemaUnion()); + } + + public ExportBuilder as(Object as) { + this.export.getAs().withObject(as); + return this; + } + + public ExportBuilder as(String as) { + this.export.getAs().withString(as); + return this; + } + + public ExportBuilder schema(String schema) { + this.export + .getSchema() + .setSchemaExternal( + new SchemaExternal() + .withResource( + new ExternalResource() + .withEndpoint( + new Endpoint() + .withUriTemplate(UriTemplateBuilder.newUriTemplate(schema))))); + return this; + } + + public ExportBuilder schema(Object schema) { + this.export.getSchema().setSchemaInline(new SchemaInline(schema)); + return this; + } + + public Export build() { + return this.export; + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenTaskBuilder.java index 5c722cb5..43e52020 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenTaskBuilder.java @@ -15,120 +15,62 @@ */ package io.serverlessworkflow.fluent.spec; -import io.serverlessworkflow.api.types.AllEventConsumptionStrategy; -import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; import io.serverlessworkflow.api.types.CorrelateProperty; -import io.serverlessworkflow.api.types.EventFilter; -import io.serverlessworkflow.api.types.EventFilterCorrelate; import io.serverlessworkflow.api.types.ListenTask; import io.serverlessworkflow.api.types.ListenTaskConfiguration; import io.serverlessworkflow.api.types.ListenTo; -import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; -import java.util.List; import java.util.function.Consumer; /** * Fluent builder for a "listen" task in a Serverless Workflow. Enforces exactly one consumption * strategy: one, all, or any. */ -public class ListenTaskBuilder extends TaskBaseBuilder { +public class ListenTaskBuilder> + extends TaskBaseBuilder> { private final ListenTask listenTask; private final ListenTaskConfiguration config; - private boolean oneSet, allSet, anySet; + private final T taskItemListBuilder; - public ListenTaskBuilder() { + public ListenTaskBuilder(T taskItemListBuilder) { super(); this.listenTask = new ListenTask(); this.config = new ListenTaskConfiguration(); this.config.setTo(new ListenTo()); this.listenTask.setListen(config); + this.taskItemListBuilder = taskItemListBuilder; super.setTask(listenTask); } @Override - protected ListenTaskBuilder self() { + protected ListenTaskBuilder self() { return this; } - /** Consume exactly one matching event. */ - public ListenTaskBuilder one(Consumer c) { - ensureNoneSet(); - oneSet = true; - EventFilterBuilder fb = new EventFilterBuilder(); - c.accept(fb); - OneEventConsumptionStrategy strat = new OneEventConsumptionStrategy(); - strat.setOne(fb.build()); - config.getTo().withOneEventConsumptionStrategy(strat); + public ListenTaskBuilder forEach(Consumer> c) { + final SubscriptionIteratorBuilder iteratorBuilder = + new SubscriptionIteratorBuilder<>(this.taskItemListBuilder); + c.accept(iteratorBuilder); + this.listenTask.setForeach(iteratorBuilder.build()); return this; } - /** Consume events only when *all* filters match. */ - public ListenTaskBuilder all(Consumer c) { - ensureNoneSet(); - allSet = true; - EventFilterBuilder fb = new EventFilterBuilder(); - c.accept(fb); - AllEventConsumptionStrategy strat = new AllEventConsumptionStrategy(); - strat.setAll(List.of(fb.build())); - config.getTo().withAllEventConsumptionStrategy(strat); + public ListenTaskBuilder read(ListenTaskConfiguration.ListenAndReadAs listenAndReadAs) { + this.config.setRead(listenAndReadAs); return this; } - /** Consume events when *any* filter matches. */ - public ListenTaskBuilder any(Consumer c) { - ensureNoneSet(); - anySet = true; - EventFilterBuilder fb = new EventFilterBuilder(); - c.accept(fb); - AnyEventConsumptionStrategy strat = new AnyEventConsumptionStrategy(); - strat.setAny(List.of(fb.build())); - config.getTo().withAnyEventConsumptionStrategy(strat); + public ListenTaskBuilder to(Consumer c) { + final ListenToBuilder listenToBuilder = new ListenToBuilder(); + c.accept(listenToBuilder); + this.config.setTo(listenToBuilder.build()); return this; } - private void ensureNoneSet() { - if (oneSet || allSet || anySet) { - throw new IllegalStateException("Only one consumption strategy can be configured"); - } - } - - /** Validate and return the built ListenTask. */ public ListenTask build() { - if (!(oneSet || allSet || anySet)) { - throw new IllegalStateException( - "A consumption strategy (one, all, or any) must be configured"); - } return listenTask; } - /** Builder for event filters used in consumption strategies. */ - public static final class EventFilterBuilder { - private final EventFilter filter = new EventFilter(); - private final EventFilterCorrelate correlate = new EventFilterCorrelate(); - - /** Predicate to match event properties. */ - public EventFilterBuilder with(Consumer c) { - EventPropertiesBuilder pb = new EventPropertiesBuilder(); - c.accept(pb); - filter.setWith(pb.build()); - return this; - } - - /** Correlation property for the filter. */ - public EventFilterBuilder correlate(String key, Consumer c) { - CorrelatePropertyBuilder cpb = new CorrelatePropertyBuilder(); - c.accept(cpb); - correlate.withAdditionalProperty(key, cpb.build()); - return this; - } - - public EventFilter build() { - filter.setCorrelate(correlate); - return filter; - } - } - public static final class CorrelatePropertyBuilder { private final CorrelateProperty prop = new CorrelateProperty(); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java new file mode 100644 index 00000000..f78f473c --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java @@ -0,0 +1,55 @@ +/* + * 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 io.serverlessworkflow.api.types.AllEventConsumptionStrategy; +import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; +import io.serverlessworkflow.api.types.ListenTo; +import io.serverlessworkflow.api.types.OneEventConsumptionStrategy; +import io.serverlessworkflow.api.types.Until; + +public class ListenToBuilder + extends AbstractEventConsumptionStrategyBuilder { + + private final ListenTo listenTo = new ListenTo(); + + ListenToBuilder() {} + + @Override + void setOne(OneEventConsumptionStrategy strategy) { + this.listenTo.setOneEventConsumptionStrategy(strategy); + } + + @Override + void setAll(AllEventConsumptionStrategy strategy) { + this.listenTo.setAllEventConsumptionStrategy(strategy); + } + + @Override + void setAny(AnyEventConsumptionStrategy strategy) { + this.listenTo.setAnyEventConsumptionStrategy(strategy); + } + + @Override + ListenTo getEventConsumptionStrategy() { + return this.listenTo; + } + + @Override + void setUntil(Until until) { + this.listenTo.getAnyEventConsumptionStrategy().setUntil(until); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java new file mode 100644 index 00000000..0c9d509b --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.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.spec; + +import io.serverlessworkflow.api.types.SubscriptionIterator; +import io.serverlessworkflow.fluent.spec.spi.SubscriptionIteratorFluent; +import java.util.function.Consumer; + +public class SubscriptionIteratorBuilder> + implements SubscriptionIteratorFluent, T> { + + private final SubscriptionIterator subscriptionIterator; + private final T taskItemListBuilder; + + public SubscriptionIteratorBuilder(T taskItemListBuilder) { + subscriptionIterator = new SubscriptionIterator(); + this.taskItemListBuilder = taskItemListBuilder; + } + + @Override + public SubscriptionIteratorBuilder item(String item) { + subscriptionIterator.setItem(item); + return this; + } + + @Override + public SubscriptionIteratorBuilder at(String at) { + subscriptionIterator.setAt(at); + return this; + } + + @Override + public SubscriptionIteratorBuilder tasks(Consumer doBuilderConsumer) { + final T taskItemListBuilder = this.taskItemListBuilder.newItemListBuilder(); + doBuilderConsumer.accept(taskItemListBuilder); + this.subscriptionIterator.setDo(taskItemListBuilder.build()); + return this; + } + + @Override + public SubscriptionIteratorBuilder output(Consumer outputConsumer) { + final OutputBuilder builder = new OutputBuilder(); + outputConsumer.accept(builder); + this.subscriptionIterator.setOutput(builder.build()); + return this; + } + + @Override + public SubscriptionIteratorBuilder export(Consumer exportConsumer) { + final ExportBuilder builder = new ExportBuilder(); + exportConsumer.accept(builder); + this.subscriptionIterator.setExport(builder.build()); + return this; + } + + @Override + public SubscriptionIteratorBuilder exportAs(Object exportAs) { + this.subscriptionIterator.setExport(new ExportBuilder().as(exportAs).build()); + return this; + } + + public SubscriptionIterator build() { + return subscriptionIterator; + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java index 3ce5c203..cd6e3a8e 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskBaseBuilder.java @@ -15,23 +15,18 @@ */ package io.serverlessworkflow.fluent.spec; -import io.serverlessworkflow.api.types.Endpoint; import io.serverlessworkflow.api.types.Export; -import io.serverlessworkflow.api.types.ExportAs; -import io.serverlessworkflow.api.types.ExternalResource; import io.serverlessworkflow.api.types.FlowDirective; import io.serverlessworkflow.api.types.FlowDirectiveEnum; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.Output; -import io.serverlessworkflow.api.types.SchemaExternal; -import io.serverlessworkflow.api.types.SchemaInline; -import io.serverlessworkflow.api.types.SchemaUnion; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.fluent.spec.spi.OutputFluent; import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; import java.util.function.Consumer; public abstract class TaskBaseBuilder> - implements TransformationHandlers { + implements TransformationHandlers, OutputFluent { private TaskBase task; protected TaskBaseBuilder() {} @@ -80,6 +75,11 @@ public T then(FlowDirectiveEnum then) { return self(); } + public T then(String taskName) { + this.task.setThen(new FlowDirective().withString(taskName)); + return self(); + } + public T exportAs(Object exportAs) { this.task.setExport(new ExportBuilder().as(exportAs).build()); return self(); @@ -108,45 +108,4 @@ public T output(Consumer outputConsumer) { // TODO: add timeout, metadata - public static final class ExportBuilder { - private final Export export; - - public ExportBuilder() { - this.export = new Export(); - this.export.setAs(new ExportAs()); - this.export.setSchema(new SchemaUnion()); - } - - public ExportBuilder as(Object as) { - this.export.getAs().withObject(as); - return this; - } - - public ExportBuilder as(String as) { - this.export.getAs().withString(as); - return this; - } - - public ExportBuilder schema(String schema) { - this.export - .getSchema() - .setSchemaExternal( - new SchemaExternal() - .withResource( - new ExternalResource() - .withEndpoint( - new Endpoint() - .withUriTemplate(UriTemplateBuilder.newUriTemplate(schema))))); - return this; - } - - public ExportBuilder schema(Object schema) { - this.export.getSchema().setSchemaInline(new SchemaInline(schema)); - return this; - } - - public Export build() { - return this.export; - } - } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java index 4c82f62a..b7fef28b 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/TaskItemListBuilder.java @@ -91,9 +91,11 @@ public TaskItemListBuilder fork(String name, Consumer itemsConf } @Override - public TaskItemListBuilder listen(String name, Consumer itemsConfigurer) { + public TaskItemListBuilder listen( + String name, Consumer> itemsConfigurer) { requireNameAndConfig(name, itemsConfigurer); - final ListenTaskBuilder listenBuilder = new ListenTaskBuilder(); + final ListenTaskBuilder listenBuilder = + new ListenTaskBuilder<>(newItemListBuilder()); itemsConfigurer.accept(listenBuilder); return addTaskItem(new TaskItem(name, new Task().withListenTask(listenBuilder.build()))); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java index a18a08bf..11631f0b 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/DoFluent.java @@ -41,5 +41,5 @@ public interface DoFluent EmitFluent, ForEachFluent, T>, ForkFluent, - ListenFluent, + ListenFluent, T>, RaiseFluent {} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java new file mode 100644 index 00000000..4db05af1 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.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.spec.spi; + +import io.serverlessworkflow.fluent.spec.EventConsumptionStrategyBuilder; +import io.serverlessworkflow.fluent.spec.EventFilterBuilder; +import java.io.Serializable; +import java.util.function.Consumer; + +public interface EventConsumptionStrategyFluent< + SELF extends EventConsumptionStrategyFluent, T extends Serializable> { + + SELF one(Consumer c); + + SELF all(Consumer c); + + SELF any(Consumer c); + + SELF until(Consumer c); + + SELF until(String expression); + + T build(); +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java index 4ca6d323..00d77036 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java @@ -18,20 +18,16 @@ import io.serverlessworkflow.api.types.ForTask; import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; -import java.util.function.Consumer; public interface ForEachTaskFluent< - SELF extends TaskBaseBuilder, L extends BaseTaskItemListBuilder> { + SELF extends TaskBaseBuilder, L extends BaseTaskItemListBuilder> + extends IteratorFluent { SELF each(String each); SELF in(String in); - SELF at(String at); - SELF whileC(final String expression); - SELF tasks(Consumer doBuilderConsumer); - ForTask build(); } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/IteratorFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/IteratorFluent.java new file mode 100644 index 00000000..85c61777 --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/IteratorFluent.java @@ -0,0 +1,35 @@ +/* + * 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.spi; + +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; +import java.util.function.Consumer; + +public interface IteratorFluent> { + + /** + * The name of the variable used to store the index of the current item being enumerated. Defaults + * to index. + */ + SELF at(String at); + + /** + * `do` in the specification. + * + *

The tasks to perform for each consumed item. + */ + SELF tasks(Consumer doBuilderConsumer); +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ListenFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ListenFluent.java index c3d32e14..ec950456 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ListenFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ListenFluent.java @@ -19,7 +19,7 @@ import java.util.UUID; import java.util.function.Consumer; -public interface ListenFluent, LIST> { +public interface ListenFluent, LIST> { LIST listen(String name, Consumer itemsConfigurer); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java new file mode 100644 index 00000000..cc49a43c --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java @@ -0,0 +1,29 @@ +/* + * 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.spi; + +import io.serverlessworkflow.fluent.spec.ExportBuilder; +import io.serverlessworkflow.fluent.spec.OutputBuilder; +import java.util.function.Consumer; + +public interface OutputFluent { + + SELF output(Consumer outputConsumer); + + SELF export(Consumer exportConsumer); + + SELF exportAs(Object exportAs); +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/SubscriptionIteratorFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/SubscriptionIteratorFluent.java new file mode 100644 index 00000000..8323833a --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/SubscriptionIteratorFluent.java @@ -0,0 +1,24 @@ +/* + * 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.spi; + +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; + +public interface SubscriptionIteratorFluent> + extends IteratorFluent, OutputFluent { + + SELF item(String item); +} 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 dbb9f1d3..cfaeb260 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 @@ -183,7 +183,13 @@ void testDoTaskListenOne() { d -> d.listen( "waitCheck", - l -> l.one(f -> f.with(p -> p.type("com.fake.pet").source("mySource"))))) + l -> + l.to( + to -> + to.one( + f -> + f.with( + p -> p.type("com.fake.pet").source("mySource")))))) .build(); List items = wf.getDo(); diff --git a/impl/jackson/src/main/java/io/serverlessworkflow/impl/expressions/jq/JacksonModelCollection.java b/impl/jackson/src/main/java/io/serverlessworkflow/impl/expressions/jq/JacksonModelCollection.java index b5420419..456db165 100644 --- a/impl/jackson/src/main/java/io/serverlessworkflow/impl/expressions/jq/JacksonModelCollection.java +++ b/impl/jackson/src/main/java/io/serverlessworkflow/impl/expressions/jq/JacksonModelCollection.java @@ -55,7 +55,7 @@ public boolean isEmpty() { @Override public boolean contains(Object o) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("contains() is not supported yet"); } @Override @@ -85,12 +85,12 @@ public WorkflowModel next() { @Override public Object[] toArray() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("toArray() is not supported yet"); } @Override public T[] toArray(T[] a) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("toArray() is not supported yet"); } @Override @@ -109,7 +109,7 @@ public boolean remove(Object o) { @Override public boolean containsAll(Collection c) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("containsAll() is not supported yet"); } @Override @@ -127,7 +127,7 @@ public boolean removeAll(Collection c) { @Override public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("retainAll() is not supported yet"); } @Override