From 6e1a3a982c18044a5227d75a36976f4877776c6e Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 8 Aug 2025 17:24:09 -0400 Subject: [PATCH 1/9] Introduce ChatBot example Signed-off-by: Ricardo Zanini --- .../agentic/AgenticModelFactory.java | 29 ++++++++--- .../fluent/agentic/AgentDoTaskBuilder.java | 7 +++ .../agentic/AgentTaskItemListBuilder.java | 8 +++ .../fluent/agentic/Agents.java | 10 ++++ .../fluent/agentic/ChatBotIT.java | 49 +++++++++++++++++++ .../fluent/func/FuncDoTaskBuilder.java | 6 +++ .../fluent/func/FuncEmitTaskBuilder.java | 4 +- .../fluent/func/FuncListenTaskBuilder.java | 29 +++++++++++ .../fluent/func/FuncTaskItemListBuilder.java | 10 ++++ .../fluent/func/spi/FuncDoFluent.java | 5 +- .../fluent/spec/TaskBaseBuilder.java | 5 ++ .../fluent/spec/spi/ListenFluent.java | 2 +- 12 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java create mode 100644 fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java 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..1d7089fe 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 @@ -28,6 +28,19 @@ class AgenticModelFactory implements WorkflowModelFactory { + private static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; + private final AgenticScopeRegistryAssessor scopeRegistryAssessor = + new AgenticScopeRegistryAssessor(); + + private AgenticModel asAgenticModel(Object value) { + // TODO: fetch memoryId from the object based on known premises + final AgenticScope agenticScope = this.scopeRegistryAssessor.getAgenticScope(); + if (value != null) { + agenticScope.writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + } + return new AgenticModel(agenticScope); + } + /** * 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 @@ -60,44 +73,46 @@ public WorkflowModelCollection createCollection() { @Override public WorkflowModel from(boolean value) { - return new JavaModel(value); + return asAgenticModel(value); } @Override public WorkflowModel from(Number value) { - return new JavaModel(value); + return asAgenticModel(value); } @Override public WorkflowModel from(String value) { - return new JavaModel(value); + return asAgenticModel(value); } @Override public WorkflowModel from(CloudEvent ce) { + // TODO: serialize the CE into the AgenticScope return new JavaModel(ce); } @Override public WorkflowModel from(CloudEventData ce) { + // TODO: serialize the CE data into the AgenticScope return new JavaModel(ce); } @Override public WorkflowModel from(OffsetDateTime value) { - return new JavaModel(value); + return asAgenticModel(value); } @Override public WorkflowModel from(Map map) { - final AgenticScope agenticScope = new AgenticScopeRegistryAssessor().getAgenticScope(); + final AgenticScope agenticScope = this.scopeRegistryAssessor.getAgenticScope(); agenticScope.writeStates(map); return new AgenticModel(agenticScope); } @Override public WorkflowModel fromNull() { - return new JavaModel(null); + return asAgenticModel(null); } @Override @@ -105,6 +120,6 @@ public WorkflowModel fromOther(Object value) { if (value instanceof AgenticScope) { return new AgenticModel((AgenticScope) value); } - return new JavaModel(value); + return asAgenticModel(value); } } 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..5dddd851 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 @@ -20,6 +20,7 @@ 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.func.spi.ConditionalTaskBuilder; @@ -81,6 +82,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/AgentTaskItemListBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java index a26f0b45..0b164a0d 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 @@ -24,6 +24,7 @@ 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.func.FuncTaskItemListBuilder; @@ -113,6 +114,13 @@ public AgentTaskItemListBuilder emit(String name, Consumer return self(); } + @Override + public AgentTaskItemListBuilder listen( + String name, Consumer itemsConfigurer) { + this.delegate.listen(name, itemsConfigurer); + return self(); + } + @Override public AgentTaskItemListBuilder forEach( String name, Consumer itemsConfigurer) { 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..dde27919 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 @@ -17,12 +17,22 @@ import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; public interface Agents { + @SystemMessage( + """ + You are a happy chat bot. + """) + interface ChatBot { + String chat(@MemoryId String memoryId, @V("message") String message); + } + interface MovieExpert { @UserMessage( 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..0f653b4a --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.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.agentic; + +import static org.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.scope.AgenticScope; +import org.junit.jupiter.api.Test; + +public class ChatBotIT { + + @Test + void chat_bot() { + Agents.ChatBot chatBot = + spy( + AgenticServices.agentBuilder(Agents.ChatBot.class) + .chatModel(Models.BASE_MODEL) + .outputName("message") + .build()); + // 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 + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.listen( + "listenToMessages", + l -> + l.outputAs(null) + .one(c -> c.with(event -> event.type("org.acme.chatbot")))) + .when(scope -> !"".equals(scope.readState("message")), AgenticScope.class) + .agent(chatBot) + .then("listenToMessages")); + } +} 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..54a82ed0 --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.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; + +import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; +import io.serverlessworkflow.fluent.func.spi.FuncTransformations; +import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; + +public class FuncListenTaskBuilder extends ListenTaskBuilder + implements ConditionalTaskBuilder, + FuncTransformations { + + FuncListenTaskBuilder() { + super(); + } +} 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/FuncDoFluent.java b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/FuncDoFluent.java index 434304d5..b452f5df 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,11 +19,13 @@ 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; @@ -36,7 +38,8 @@ public interface FuncDoFluent> EmitFluent, ForEachFluent, SwitchFluent, - ForkFluent { + ForkFluent, + ListenFluent { SELF callFn(String name, Consumer cfg); 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..817f7828 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 @@ -80,6 +80,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(); 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); From 273aaef372486df529df4fee490430747d2ee49b Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 11 Aug 2025 19:43:19 -0400 Subject: [PATCH 2/9] Refactor agentic to hold scope and current workflow context Signed-off-by: Ricardo Zanini --- experimental/agentic/pom.xml | 2 +- .../expressions/agentic/AgenticModel.java | 20 +- .../agentic/AgenticModelCollection.java | 55 +++++- .../agentic/AgenticModelFactory.java | 73 ++++--- .../AgenticScopeRegistryAssessor.java | 9 +- .../expressions/func/JavaModelCollection.java | 10 +- .../expressions/func/JavaModelFactory.java | 2 +- .../WorkflowInvocationHandler.java | 10 +- .../fluent/agentic/Agents.java | 10 +- .../fluent/agentic/ChatBotIT.java | 179 ++++++++++++++++-- .../fluent/agentic/Models.java | 2 +- .../fluent/agentic/WorkflowTests.java | 26 +-- .../jq/JacksonModelCollection.java | 10 +- 13 files changed, 301 insertions(+), 107 deletions(-) diff --git a/experimental/agentic/pom.xml b/experimental/agentic/pom.xml index dec8a5e5..2d648084 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 + Serveless 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..68a17800 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 @@ -15,29 +15,66 @@ */ package io.serverlessworkflow.impl.expressions.agentic; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.agentic.scope.AgenticScope; import dev.langchain4j.agentic.scope.ResultWithAgenticScope; +import io.cloudevents.CloudEvent; +import io.cloudevents.CloudEventData; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.expressions.func.JavaModelCollection; -import java.util.Collection; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; import java.util.Optional; -class AgenticModelCollection extends JavaModelCollection { +public class AgenticModelCollection extends JavaModelCollection { private final AgenticScope agenticScope; - - AgenticModelCollection(Collection object, AgenticScope agenticScope) { - super(object); - this.agenticScope = agenticScope; - } + private final ObjectMapper mapper = new ObjectMapper(); AgenticModelCollection(AgenticScope agenticScope) { + super(Collections.emptyList()); this.agenticScope = agenticScope; } @Override - protected WorkflowModel nextItem(Object obj) { - return new AgenticModel((AgenticScope) obj); + public boolean add(WorkflowModel e) { + Optional> asMap = e.asMap(); + if (asMap.isPresent()) { + this.agenticScope.writeStates(asMap.get()); + } else { + // Update the agenticScope with the event body, so agents can use the event data as input + Object javaObj = e.asJavaObject(); + if (javaObj instanceof CloudEvent) { + try { + this.agenticScope.writeStates( + mapper.readValue( + Objects.requireNonNull(((CloudEvent) javaObj).getData()).toString(), + new TypeReference<>() {})); + } catch (JsonProcessingException ex) { + throw new IllegalArgumentException( + "Unable to parse CloudEvent, data must be a valid JSON", ex); + } + } else if (javaObj instanceof CloudEventData) { + try { + this.agenticScope.writeStates( + mapper.readValue( + Objects.requireNonNull(((CloudEventData) javaObj)).toBytes(), + new TypeReference<>() {})); + } catch (IOException ex) { + throw new IllegalArgumentException( + "Unable to parse CloudEventData, data must be a valid JSON", ex); + } + } else { + this.agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, javaObj); + } + } + + // add to the collection + return super.add(e); } @Override 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 1d7089fe..00bc4e0a 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,38 +22,31 @@ 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 { - private static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; + static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; private final AgenticScopeRegistryAssessor scopeRegistryAssessor = new AgenticScopeRegistryAssessor(); - private AgenticModel asAgenticModel(Object value) { - // TODO: fetch memoryId from the object based on known premises - final AgenticScope agenticScope = this.scopeRegistryAssessor.getAgenticScope(); - if (value != null) { - agenticScope.writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - } - return new AgenticModel(agenticScope); - } - - /** - * 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. - */ @Override + @SuppressWarnings("unchecked") 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()); + } + + if (obj instanceof Map) { + this.scopeRegistryAssessor.getAgenticScope().writeStates((Map) obj); + } else { + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, obj); + } + + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), obj); } @Override @@ -66,60 +59,60 @@ public WorkflowModel combine(Map workflowVariables) { @Override public WorkflowModelCollection createCollection() { - throw new UnsupportedOperationException(); + return new AgenticModelCollection(this.scopeRegistryAssessor.getAgenticScope()); } - // TODO: all these methods can use agenticScope as long as we have access to the `outputName` - @Override public WorkflowModel from(boolean value) { - return asAgenticModel(value); + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); } @Override public WorkflowModel from(Number value) { - return asAgenticModel(value); + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); } @Override public WorkflowModel from(String value) { - return asAgenticModel(value); + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); } @Override public WorkflowModel from(CloudEvent ce) { - // TODO: serialize the CE into the AgenticScope - return new JavaModel(ce); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), ce); } @Override public WorkflowModel from(CloudEventData ce) { - // TODO: serialize the CE data into the AgenticScope - return new JavaModel(ce); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), ce); } @Override public WorkflowModel from(OffsetDateTime value) { - return asAgenticModel(value); + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); } @Override public WorkflowModel from(Map map) { - final AgenticScope agenticScope = this.scopeRegistryAssessor.getAgenticScope(); - agenticScope.writeStates(map); - return new AgenticModel(agenticScope); + this.scopeRegistryAssessor.getAgenticScope().writeStates(map); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), map); } @Override public WorkflowModel fromNull() { - return asAgenticModel(null); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), 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 asAgenticModel(value); + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); } } 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..34a959c3 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,6 +16,7 @@ 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.Objects; @@ -27,7 +28,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 +45,7 @@ public void setMemoryId(Object memoryId) { this.memoryId = memoryId; } - public DefaultAgenticScope getAgenticScope() { + public AgenticScope getAgenticScope() { if (agenticScope != null) { return agenticScope; } @@ -57,6 +58,10 @@ public DefaultAgenticScope getAgenticScope() { return this.agenticScope; } + public void setAgenticScope(AgenticScope agenticScope) { + this.agenticScope = agenticScope; + } + @Override public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { this.agenticScope = agenticScope; 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/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index dde27919..c1c17020 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 @@ -18,18 +18,18 @@ import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; import dev.langchain4j.service.MemoryId; -import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; public interface Agents { - @SystemMessage( - """ - You are a happy chat bot. - """) interface ChatBot { + @UserMessage( + """ + You are a happy chat bot. + """) + @Agent String chat(@MemoryId String memoryId, @V("message") String message); } 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 index 0f653b4a..38e92487 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -15,35 +15,192 @@ */ package io.serverlessworkflow.fluent.agentic; +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.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.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +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("message") .build()); + Collection publishedEvents = new ArrayList<>(); + // 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 - AgentWorkflowBuilder.workflow("chat-bot") - .tasks( - t -> - t.listen( - "listenToMessages", - l -> - l.outputAs(null) - .one(c -> c.with(event -> event.type("org.acme.chatbot")))) - .when(scope -> !"".equals(scope.readState("message")), AgenticScope.class) - .agent(chatBot) - .then("listenToMessages")); + final Workflow listenWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.listen( + "listenToMessages", + l -> + l.one(c -> c.with(event -> event.type("org.acme.chatbot.request")))) + .when(message -> !"".equals(message.get("message")), Map.class) + .agent(chatBot) + .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.reply"))) + .then("listenToMessages")) + .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 -> publishedEvents.add((CloudEvent) ce)); + + final WorkflowInstance waitingInstance = + app.workflowDefinition(listenWorkflow).instance(null); + final CompletableFuture runningModel = waitingInstance.start(); + + // The workflow is just waiting for the event + assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); + + // Publish the event + app.eventPublisher().publish(newMessageEvent("Hello World!")); + + AgenticScope scope = runningModel.get().as(AgenticScope.class).orElseThrow(); + assertNotNull(scope.readState("message")); + assertFalse(scope.readState("message").toString().isEmpty()); + assertEquals(1, publishedEvents.size()); + + // We ingested the event, and we keep waiting for the next + // assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); + + // Publish the event with an empty message to wrap up + app.eventPublisher().publish(newMessageEvent("")); + + scope = runningModel.join().as(AgenticScope.class).orElseThrow(); + assertNotNull(scope.readState("message")); + assertTrue(scope.readState("message").toString().isEmpty()); + assertEquals(2, publishedEvents.size()); + + // Workflow should be done + assertEquals(WorkflowStatus.COMPLETED, waitingInstance.status()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + /** + * 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("message") + .build()); + + final Workflow mixedWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.callFn( + callJ -> + callJ.function( + input -> { + System.out.println(input); + return Map.of("message", 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 new CloudEventBuilder() + .withData(String.format("{\"message\": \"%s\"}", message).getBytes()) + .withType("org.acme.chatbot.request") + .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/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 From 665441ecf78f692b97c3599db63402a5d8f60ecd Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 12 Aug 2025 15:06:37 -0400 Subject: [PATCH 3/9] Small refactor Signed-off-by: Ricardo Zanini --- .../agentic/AgenticModelCollection.java | 40 ++++++---------- .../agentic/AgenticModelFactory.java | 48 ++++++++++++------- .../AgenticScopeRegistryAssessor.java | 4 +- 3 files changed, 46 insertions(+), 46 deletions(-) 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 68a17800..4b23e690 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 @@ -15,7 +15,6 @@ */ package io.serverlessworkflow.impl.expressions.agentic; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.agentic.scope.AgenticScope; @@ -27,7 +26,6 @@ import java.io.IOException; import java.util.Collections; import java.util.Map; -import java.util.Objects; import java.util.Optional; public class AgenticModelCollection extends JavaModelCollection { @@ -45,32 +43,22 @@ public boolean add(WorkflowModel e) { Optional> asMap = e.asMap(); if (asMap.isPresent()) { this.agenticScope.writeStates(asMap.get()); - } else { - // Update the agenticScope with the event body, so agents can use the event data as input - Object javaObj = e.asJavaObject(); - if (javaObj instanceof CloudEvent) { - try { - this.agenticScope.writeStates( - mapper.readValue( - Objects.requireNonNull(((CloudEvent) javaObj).getData()).toString(), - new TypeReference<>() {})); - } catch (JsonProcessingException ex) { - throw new IllegalArgumentException( - "Unable to parse CloudEvent, data must be a valid JSON", ex); - } - } else if (javaObj instanceof CloudEventData) { - try { - this.agenticScope.writeStates( - mapper.readValue( - Objects.requireNonNull(((CloudEventData) javaObj)).toBytes(), - new TypeReference<>() {})); - } catch (IOException ex) { - throw new IllegalArgumentException( - "Unable to parse CloudEventData, data must be a valid JSON", ex); - } + return super.add(e); + } + + // Update the agenticScope with the event body, so agents can use the event data as input + Object javaObj = e.asJavaObject(); + try { + if (javaObj instanceof CloudEvent ce && ce.getData() != null) { + agenticScope.writeStates( + mapper.readValue(ce.getData().toBytes(), new TypeReference<>() {})); + } else if (javaObj instanceof CloudEventData ced) { + agenticScope.writeStates(mapper.readValue(ced.toBytes(), new TypeReference<>() {})); } else { - this.agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, javaObj); + agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, javaObj); } + } catch (IOException ex) { + throw new IllegalArgumentException("Unable to parse CloudEvent data as JSON", ex); } // add to the collection 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 00bc4e0a..67391ae8 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 @@ -31,6 +31,18 @@ class AgenticModelFactory implements WorkflowModelFactory { private final AgenticScopeRegistryAssessor scopeRegistryAssessor = new AgenticScopeRegistryAssessor(); + private void updateAgenticScope(Object value) { + this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); + } + + private void updateAgenticScope(Map state) { + this.scopeRegistryAssessor.getAgenticScope().writeStates(state); + } + + private AgenticModel asAgenticModel(Object value) { + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + } + @Override @SuppressWarnings("unchecked") public WorkflowModel fromAny(WorkflowModel prev, Object obj) { @@ -41,12 +53,12 @@ public WorkflowModel fromAny(WorkflowModel prev, Object obj) { } if (obj instanceof Map) { - this.scopeRegistryAssessor.getAgenticScope().writeStates((Map) obj); + this.updateAgenticScope((Map) obj); } else { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, obj); + this.updateAgenticScope(obj); } - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), obj); + return asAgenticModel(obj); } @Override @@ -64,47 +76,47 @@ public WorkflowModelCollection createCollection() { @Override public WorkflowModel from(boolean value) { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + this.updateAgenticScope(value); + return asAgenticModel(value); } @Override public WorkflowModel from(Number value) { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + this.updateAgenticScope(value); + return asAgenticModel(value); } @Override public WorkflowModel from(String value) { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + this.updateAgenticScope(value); + return asAgenticModel(value); } @Override public WorkflowModel from(CloudEvent ce) { - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), ce); + return asAgenticModel(ce); } @Override public WorkflowModel from(CloudEventData ce) { - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), ce); + return asAgenticModel(ce); } @Override public WorkflowModel from(OffsetDateTime value) { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + this.updateAgenticScope(value); + return asAgenticModel(value); } @Override public WorkflowModel from(Map map) { - this.scopeRegistryAssessor.getAgenticScope().writeStates(map); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), map); + this.updateAgenticScope(map); + return asAgenticModel(map); } @Override public WorkflowModel fromNull() { - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), null); + return asAgenticModel(null); } @Override @@ -112,7 +124,7 @@ public WorkflowModel fromOther(Object value) { if (value instanceof AgenticScope scope) { return new AgenticModel(scope, scope.state()); } - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + this.updateAgenticScope(value); + return asAgenticModel(value); } } 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 34a959c3..390a3a42 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 @@ -59,12 +59,12 @@ public AgenticScope getAgenticScope() { } public void setAgenticScope(AgenticScope agenticScope) { - this.agenticScope = agenticScope; + this.agenticScope = Objects.requireNonNull(agenticScope, "AgenticScope cannot be null"); } @Override public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { - this.agenticScope = agenticScope; + this.setAgenticScope(agenticScope); return this; } From aa19288a8e7cd6f99bb5b7a90471bbd597658070 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Wed, 13 Aug 2025 12:55:34 +0200 Subject: [PATCH 4/9] Adding until to listen Signed-off-by: fjtirado --- .../fluent/agentic/ChatBotIT.java | 10 +++++---- .../fluent/func/FuncListenTaskBuilder.java | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) 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 index 38e92487..44884266 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -70,11 +70,13 @@ void chat_bot() { t.listen( "listenToMessages", l -> - l.one(c -> c.with(event -> event.type("org.acme.chatbot.request")))) - .when(message -> !"".equals(message.get("message")), Map.class) + l.until(message -> "".equals(message.get("message")), Map.class) + .one( + c -> + c.with( + event -> event.type("org.acme.chatbot.request")))) .agent(chatBot) - .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.reply"))) - .then("listenToMessages")) + .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.reply")))) .build(); try (WorkflowApplication app = WorkflowApplication.builder().build()) { 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 index 54a82ed0..bcdd82dd 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java @@ -15,15 +15,37 @@ */ 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(); } + + 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; + } } From fca26604e6955550f3891dd9e36a56fc38c919d3 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 15 Aug 2025 10:36:10 -0400 Subject: [PATCH 5/9] Adjusting model to ingest CE Signed-off-by: Ricardo Zanini --- experimental/agentic/pom.xml | 2 +- .../agentic/AgenticModelCollection.java | 2 +- .../impl/expressions/func/JavaModel.java | 1 + .../fluent/agentic/ChatBotIT.java | 23 +++++++++++-------- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/experimental/agentic/pom.xml b/experimental/agentic/pom.xml index 2d648084..855838c5 100644 --- a/experimental/agentic/pom.xml +++ b/experimental/agentic/pom.xml @@ -7,7 +7,7 @@ 8.0.0-SNAPSHOT serverlessworkflow-experimental-agentic - Serveless Workflow :: Experimental :: Agentic + Serverless Workflow :: Experimental :: Agentic io.serverlessworkflow 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 4b23e690..af21ff36 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 @@ -41,7 +41,7 @@ public class AgenticModelCollection extends JavaModelCollection { @Override public boolean add(WorkflowModel e) { Optional> asMap = e.asMap(); - if (asMap.isPresent()) { + if (asMap.isPresent() && !asMap.get().isEmpty()) { this.agenticScope.writeStates(asMap.get()); return super.add(e); } 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/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java index 44884266..02fb9d6c 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -71,7 +71,7 @@ void chat_bot() { "listenToMessages", l -> l.until(message -> "".equals(message.get("message")), Map.class) - .one( + .any( c -> c.with( event -> event.type("org.acme.chatbot.request")))) @@ -99,24 +99,27 @@ void chat_bot() { // Publish the event app.eventPublisher().publish(newMessageEvent("Hello World!")); - AgenticScope scope = runningModel.get().as(AgenticScope.class).orElseThrow(); - assertNotNull(scope.readState("message")); - assertFalse(scope.readState("message").toString().isEmpty()); - assertEquals(1, publishedEvents.size()); - // We ingested the event, and we keep waiting for the next // assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); // Publish the event with an empty message to wrap up app.eventPublisher().publish(newMessageEvent("")); - scope = runningModel.join().as(AgenticScope.class).orElseThrow(); + // scope = runningModel.join().as(AgenticScope.class).orElseThrow(); + // assertNotNull(scope.readState("message")); + // assertTrue(scope.readState("message").toString().isEmpty()); + // assertEquals(2, publishedEvents.size()); + + Thread.sleep(30000); + assertTrue(waitingInstance.cancel()); + + AgenticScope scope = runningModel.get().as(AgenticScope.class).orElseThrow(); assertNotNull(scope.readState("message")); - assertTrue(scope.readState("message").toString().isEmpty()); - assertEquals(2, publishedEvents.size()); + assertFalse(scope.readState("message").toString().isEmpty()); + assertEquals(1, publishedEvents.size()); // Workflow should be done - assertEquals(WorkflowStatus.COMPLETED, waitingInstance.status()); + assertEquals(WorkflowStatus.CANCELLED, waitingInstance.status()); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } From d61b807fd8267e95d6acbc5080236fe13b49ce52 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 15 Aug 2025 19:05:02 -0400 Subject: [PATCH 6/9] Add listen tasks Signed-off-by: Ricardo Zanini --- .../agentic/AgenticModelCollection.java | 20 +- .../agentic/AgenticModelFactory.java | 56 ++- .../AgenticScopeCloudEventsHandler.java | 57 +++ .../AgenticScopeRegistryAssessor.java | 10 + .../fluent/agentic/AgentDoTaskBuilder.java | 3 +- .../agentic/AgentListenTaskBuilder.java | 34 ++ .../agentic/AgentTaskItemListBuilder.java | 6 +- .../fluent/agentic/spi/AgentDoFluent.java | 67 ++-- .../fluent/agentic/Agents.java | 5 +- .../fluent/agentic/ChatBotIT.java | 347 +++++++++--------- .../fluent/func/FuncListenTaskBuilder.java | 6 +- .../fluent/func/spi/CallFnFluent.java | 15 + .../fluent/func/spi/FuncDoFluent.java | 13 +- .../fluent/spec/DoTaskBuilder.java | 2 +- .../fluent/spec/ExportBuilder.java | 66 ++++ .../fluent/spec/ListenTaskBuilder.java | 26 +- .../spec/SubscriptionIteratorBuilder.java | 78 ++++ .../fluent/spec/TaskBaseBuilder.java | 50 +-- .../fluent/spec/TaskItemListBuilder.java | 4 +- .../fluent/spec/spi/DoFluent.java | 2 +- .../fluent/spec/spi/ForEachTaskFluent.java | 15 +- .../fluent/spec/spi/IteratorFluent.java | 22 ++ .../fluent/spec/spi/OutputFluent.java | 16 + .../spec/spi/SubscriptionIteratorFluent.java | 24 ++ 24 files changed, 614 insertions(+), 330 deletions(-) create mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticScopeCloudEventsHandler.java create mode 100644 fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java create mode 100644 fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/CallFnFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/IteratorFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/SubscriptionIteratorFluent.java 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 af21ff36..ca47c681 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 @@ -31,11 +31,12 @@ public class AgenticModelCollection extends JavaModelCollection { private final AgenticScope agenticScope; - private final ObjectMapper mapper = new ObjectMapper(); + private final AgenticScopeCloudEventsHandler ceHandler; - AgenticModelCollection(AgenticScope agenticScope) { + AgenticModelCollection(AgenticScope agenticScope, AgenticScopeCloudEventsHandler ceHandler) { super(Collections.emptyList()); this.agenticScope = agenticScope; + this.ceHandler = ceHandler; } @Override @@ -47,18 +48,9 @@ public boolean add(WorkflowModel e) { } // Update the agenticScope with the event body, so agents can use the event data as input - Object javaObj = e.asJavaObject(); - try { - if (javaObj instanceof CloudEvent ce && ce.getData() != null) { - agenticScope.writeStates( - mapper.readValue(ce.getData().toBytes(), new TypeReference<>() {})); - } else if (javaObj instanceof CloudEventData ced) { - agenticScope.writeStates(mapper.readValue(ced.toBytes(), new TypeReference<>() {})); - } else { - agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, javaObj); - } - } catch (IOException ex) { - throw new IllegalArgumentException("Unable to parse CloudEvent data as JSON", ex); + Object value = e.asJavaObject(); + if (!ceHandler.writeStateIfCloudEvent(this.agenticScope, value)) { + this.agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); } // add to the collection 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 67391ae8..63ca9709 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 @@ -30,35 +30,31 @@ class AgenticModelFactory implements WorkflowModelFactory { static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; private final AgenticScopeRegistryAssessor scopeRegistryAssessor = new AgenticScopeRegistryAssessor(); + private final AgenticScopeCloudEventsHandler scopeCloudEventsHandler = new AgenticScopeCloudEventsHandler(); - private void updateAgenticScope(Object value) { - this.scopeRegistryAssessor.getAgenticScope().writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - } + @SuppressWarnings("unchecked") + private AgenticModel newAgenticModel(Object state) { + if (state == null) { + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), null); + } - private void updateAgenticScope(Map state) { - this.scopeRegistryAssessor.getAgenticScope().writeStates(state); - } + if (state instanceof Map) { + this.scopeRegistryAssessor.writeStates((Map) state); + } else { + this.scopeRegistryAssessor.writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, state); + } - private AgenticModel asAgenticModel(Object value) { - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), value); + return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), state); } @Override - @SuppressWarnings("unchecked") public WorkflowModel fromAny(WorkflowModel prev, Object obj) { // 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()); } - - if (obj instanceof Map) { - this.updateAgenticScope((Map) obj); - } else { - this.updateAgenticScope(obj); - } - - return asAgenticModel(obj); + return newAgenticModel(obj); } @Override @@ -71,52 +67,47 @@ public WorkflowModel combine(Map workflowVariables) { @Override public WorkflowModelCollection createCollection() { - return new AgenticModelCollection(this.scopeRegistryAssessor.getAgenticScope()); + return new AgenticModelCollection(this.scopeRegistryAssessor.getAgenticScope(), scopeCloudEventsHandler); } @Override public WorkflowModel from(boolean value) { - this.updateAgenticScope(value); - return asAgenticModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(Number value) { - this.updateAgenticScope(value); - return asAgenticModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(String value) { - this.updateAgenticScope(value); - return asAgenticModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(CloudEvent ce) { - return asAgenticModel(ce); + return from(scopeCloudEventsHandler.extractDataAsMap(ce)); } @Override public WorkflowModel from(CloudEventData ce) { - return asAgenticModel(ce); + return from(scopeCloudEventsHandler.extractDataAsMap(ce)); } @Override public WorkflowModel from(OffsetDateTime value) { - this.updateAgenticScope(value); - return asAgenticModel(value); + return newAgenticModel(value); } @Override public WorkflowModel from(Map map) { - this.updateAgenticScope(map); - return asAgenticModel(map); + return newAgenticModel(map); } @Override public WorkflowModel fromNull() { - return asAgenticModel(null); + return newAgenticModel(null); } @Override @@ -124,7 +115,6 @@ public WorkflowModel fromOther(Object value) { if (value instanceof AgenticScope scope) { return new AgenticModel(scope, scope.state()); } - this.updateAgenticScope(value); - return asAgenticModel(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..39586093 --- /dev/null +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticScopeCloudEventsHandler.java @@ -0,0 +1,57 @@ +package io.serverlessworkflow.impl.expressions.agentic; + +import java.io.IOException; +import java.util.Map; + +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; + +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 390a3a42..05620b64 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 @@ -19,6 +19,8 @@ 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; @@ -62,6 +64,14 @@ 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.setAgenticScope(agenticScope); 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 5dddd851..cea7b9d4 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,6 +15,7 @@ */ package io.serverlessworkflow.fluent.agentic; +import dev.langchain4j.agentic.Agent; import io.serverlessworkflow.fluent.agentic.spi.AgentDoFluent; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; @@ -83,7 +84,7 @@ public AgentDoTaskBuilder emit(String name, Consumer itemsC } @Override - public AgentDoTaskBuilder listen(String name, Consumer itemsConfigurer) { + public AgentDoTaskBuilder listen(String name, Consumer itemsConfigurer) { this.listBuilder().listen(name, itemsConfigurer); return self(); } 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..ed38ee96 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java @@ -0,0 +1,34 @@ +package io.serverlessworkflow.fluent.agentic; + +import java.util.function.Predicate; + +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; + +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 0b164a0d..aff81e60 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 @@ -116,8 +116,10 @@ public AgentTaskItemListBuilder emit(String name, Consumer @Override public AgentTaskItemListBuilder listen( - String name, Consumer itemsConfigurer) { - this.delegate.listen(name, itemsConfigurer); + String name, Consumer itemsConfigurer) { + final AgentListenTaskBuilder builder = new AgentListenTaskBuilder(); + itemsConfigurer.accept(builder); + this.addTaskItem(new TaskItem(name, new Task().withListenTask(builder.build()))); return self(); } 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..89b70a58 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,40 +15,61 @@ */ package io.serverlessworkflow.fluent.agentic.spi; -import io.serverlessworkflow.fluent.agentic.LoopAgentsBuilder; -import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; import java.util.UUID; import java.util.function.Consumer; -public interface AgentDoFluent> extends FuncDoFluent { +import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; +import io.serverlessworkflow.fluent.agentic.LoopAgentsBuilder; +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; + +public interface AgentDoFluent> + extends SetFluent, + EmitFluent, + ForEachFluent, + SwitchFluent, + ForkFluent, + ListenFluent, + CallFnFluent { - SELF agent(String name, Object agent); + SELF agent(String name, Object agent); - default SELF agent(Object agent) { - return agent(UUID.randomUUID().toString(), agent); - } + default SELF agent(Object agent) { + return agent(UUID.randomUUID().toString(), agent); + } - SELF sequence(String name, Object... agents); + SELF sequence(String name, Object... agents); - default SELF sequence(Object... agents) { - return sequence("seq-" + UUID.randomUUID(), agents); - } + default SELF sequence(Object... agents) { + return sequence("seq-" + UUID.randomUUID(), agents); + } - SELF loop(String name, Consumer builder); + SELF loop(String name, Consumer builder); - default SELF loop(Consumer builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } + default SELF loop(Consumer builder) { + return loop("loop-" + UUID.randomUUID(), builder); + } - SELF loop(String name, LoopAgentsBuilder builder); + SELF loop(String name, LoopAgentsBuilder builder); - default SELF loop(LoopAgentsBuilder builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } + default SELF loop(LoopAgentsBuilder builder) { + return loop("loop-" + UUID.randomUUID(), builder); + } - SELF parallel(String name, Object... agents); + SELF parallel(String name, Object... agents); - default SELF parallel(Object... agents) { - return parallel("par-" + UUID.randomUUID(), agents); - } + default SELF parallel(Object... agents) { + return parallel("par-" + UUID.randomUUID(), agents); + } } 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 c1c17020..5e22b8f4 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 @@ -27,10 +27,11 @@ public interface Agents { interface ChatBot { @UserMessage( """ - You are a happy chat bot. + You are a happy chat bot, reply to my message: + {message}. """) @Agent - String chat(@MemoryId String memoryId, @V("message") String message); + String chat(@V("message") String message); } interface MovieExpert { 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 index 02fb9d6c..ada4c978 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -15,11 +15,18 @@ */ package io.serverlessworkflow.fluent.agentic; -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.mockito.Mockito.spy; +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; import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.scope.AgenticScope; @@ -28,184 +35,174 @@ import io.cloudevents.core.v1.CloudEventBuilder; import io.serverlessworkflow.api.types.EventFilter; import io.serverlessworkflow.api.types.EventProperties; +import io.serverlessworkflow.api.types.ListenTaskConfiguration; 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.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; + +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; 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("message") - .build()); - Collection publishedEvents = new ArrayList<>(); - - // 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( - "listenToMessages", - l -> - l.until(message -> "".equals(message.get("message")), Map.class) - .any( - c -> - c.with( - event -> event.type("org.acme.chatbot.request")))) - .agent(chatBot) - .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.reply")))) - .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 -> publishedEvents.add((CloudEvent) ce)); - - final WorkflowInstance waitingInstance = - app.workflowDefinition(listenWorkflow).instance(null); - final CompletableFuture runningModel = waitingInstance.start(); - - // The workflow is just waiting for the event - assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); - - // Publish the event - app.eventPublisher().publish(newMessageEvent("Hello World!")); - - // We ingested the event, and we keep waiting for the next - // assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); - - // Publish the event with an empty message to wrap up - app.eventPublisher().publish(newMessageEvent("")); - - // scope = runningModel.join().as(AgenticScope.class).orElseThrow(); - // assertNotNull(scope.readState("message")); - // assertTrue(scope.readState("message").toString().isEmpty()); - // assertEquals(2, publishedEvents.size()); - - Thread.sleep(30000); - assertTrue(waitingInstance.cancel()); - - AgenticScope scope = runningModel.get().as(AgenticScope.class).orElseThrow(); - assertNotNull(scope.readState("message")); - assertFalse(scope.readState("message").toString().isEmpty()); - assertEquals(1, publishedEvents.size()); - - // Workflow should be done - assertEquals(WorkflowStatus.CANCELLED, waitingInstance.status()); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); + @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("message") + .build()); + BlockingQueue publishedEvents = 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.until(message -> "".equals(message.get("message")), Map.class) + .any(c -> c.with(event -> event.type("org.acme.chatbot.request"))) + .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 -> publishedEvents.add((CloudEvent) ce)); + + app.eventConsumer().register(app.eventConsumer() + .listen( + new EventFilter() + .withWith(new EventProperties().withType("org.acme.chatbot.finished")), + app), + ce -> publishedEvents.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 event + app.eventPublisher().publish(newMessageEvent("Hello World!")); + CloudEvent reply = publishedEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(reply); + + // Empty message completes the workflow + app.eventPublisher().publish(newMessageEvent("")); + CloudEvent finished = publishedEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(finished); + + 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("message") - .build()); - - final Workflow mixedWorkflow = - AgentWorkflowBuilder.workflow("chat-bot") - .tasks( - t -> - t.callFn( - callJ -> - callJ.function( - input -> { - System.out.println(input); - return Map.of("message", 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")); + + /** + * 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("message") + .build()); + + final Workflow mixedWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.callFn( + callJ -> + callJ.function( + input -> { + System.out.println(input); + return Map.of("message", 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 new CloudEventBuilder() + .withData(String.format("{\"message\": \"%s\"}", message).getBytes()) + .withType("org.acme.chatbot.request") + .withId(UUID.randomUUID().toString()) + .withDataContentType("application/json") + .withSource(URI.create("test://localhost")) + .withSubject("A chatbot message") + .withTime(OffsetDateTime.now()) + .build(); } - } - - private CloudEvent newMessageEvent(String message) { - return new CloudEventBuilder() - .withData(String.format("{\"message\": \"%s\"}", message).getBytes()) - .withType("org.acme.chatbot.request") - .withId(UUID.randomUUID().toString()) - .withDataContentType("application/json") - .withSource(URI.create("test://localhost")) - .withSubject("A chatbot message") - .withTime(OffsetDateTime.now()) - .build(); - } } 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 index bcdd82dd..6ad28669 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java @@ -20,17 +20,19 @@ 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.BaseDoTaskBuilder; +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; import java.util.function.Predicate; -public class FuncListenTaskBuilder extends ListenTaskBuilder +public class FuncListenTaskBuilder extends ListenTaskBuilder implements ConditionalTaskBuilder, FuncTransformations { private UntilPredicate untilPredicate; FuncListenTaskBuilder() { - super(); + super(new FuncTaskItemListBuilder()); } public FuncListenTaskBuilder until(Predicate predicate, Class predClass) { 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..21b07585 --- /dev/null +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/spi/CallFnFluent.java @@ -0,0 +1,15 @@ +package io.serverlessworkflow.fluent.func.spi; + +import java.util.UUID; +import java.util.function.Consumer; + +import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; + +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 b452f5df..eb46de9b 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 @@ -28,22 +28,15 @@ 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 public interface FuncDoFluent> - extends SetFluent, + extends SetFluent, EmitFluent, ForEachFluent, SwitchFluent, ForkFluent, - ListenFluent { - - SELF callFn(String name, Consumer cfg); - - default SELF callFn(Consumer cfg) { - return this.callFn(UUID.randomUUID().toString(), cfg); - } + ListenFluent, + CallFnFluent { } 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..38c383fc 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,7 @@ 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/ExportBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java new file mode 100644 index 00000000..095a61a8 --- /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..b5bfd74f 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 @@ -31,28 +31,30 @@ * 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) { + public ListenTaskBuilder one(Consumer c) { ensureNoneSet(); oneSet = true; EventFilterBuilder fb = new EventFilterBuilder(); @@ -64,7 +66,7 @@ public ListenTaskBuilder one(Consumer c) { } /** Consume events only when *all* filters match. */ - public ListenTaskBuilder all(Consumer c) { + public ListenTaskBuilder all(Consumer c) { ensureNoneSet(); allSet = true; EventFilterBuilder fb = new EventFilterBuilder(); @@ -76,7 +78,7 @@ public ListenTaskBuilder all(Consumer c) { } /** Consume events when *any* filter matches. */ - public ListenTaskBuilder any(Consumer c) { + public ListenTaskBuilder any(Consumer c) { ensureNoneSet(); anySet = true; EventFilterBuilder fb = new EventFilterBuilder(); @@ -87,6 +89,18 @@ public ListenTaskBuilder any(Consumer c) { return this; } + public ListenTaskBuilder forEach(Consumer> c) { + final SubscriptionIteratorBuilder iteratorBuilder = new SubscriptionIteratorBuilder<>(this.taskItemListBuilder); + c.accept(iteratorBuilder); + this.listenTask.setForeach(iteratorBuilder.build()); + return this; + } + + public ListenTaskBuilder read(ListenTaskConfiguration.ListenAndReadAs listenAndReadAs) { + this.config.setRead(listenAndReadAs); + return this; + } + private void ensureNoneSet() { if (oneSet || allSet || anySet) { throw new IllegalStateException("Only one consumption strategy can be configured"); 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..1f5ca0a5 --- /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 java.util.function.Consumer; + +import io.serverlessworkflow.api.types.SubscriptionIterator; +import io.serverlessworkflow.fluent.spec.spi.SubscriptionIteratorFluent; + +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 817f7828..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() {} @@ -113,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..3edfc368 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,9 @@ 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/ForEachTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java index 4ca6d323..c233d7e1 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,15 @@ 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 each(String each); - SELF in(String in); + SELF in(String in); - SELF at(String at); + SELF whileC(final String expression); - SELF whileC(final String expression); - - SELF tasks(Consumer doBuilderConsumer); - - ForTask build(); + 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..abfe003b --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/IteratorFluent.java @@ -0,0 +1,22 @@ +package io.serverlessworkflow.fluent.spec.spi; + +import java.util.function.Consumer; + +import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; + +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/OutputFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java new file mode 100644 index 00000000..2a13399a --- /dev/null +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java @@ -0,0 +1,16 @@ +package io.serverlessworkflow.fluent.spec.spi; + +import java.util.function.Consumer; + +import io.serverlessworkflow.fluent.spec.ExportBuilder; +import io.serverlessworkflow.fluent.spec.OutputBuilder; + +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..197fd6f7 --- /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); + +} From ed9559596142576a7a0611a5155785b7747d0278 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 15 Aug 2025 19:15:04 -0400 Subject: [PATCH 7/9] Format and headers Signed-off-by: Ricardo Zanini --- .../agentic/AgenticModelCollection.java | 5 - .../agentic/AgenticModelFactory.java | 6 +- .../AgenticScopeCloudEventsHandler.java | 85 +++-- .../AgenticScopeRegistryAssessor.java | 1 - .../fluent/agentic/AgentDoTaskBuilder.java | 2 - .../agentic/AgentListenTaskBuilder.java | 53 ++- .../agentic/AgentTaskItemListBuilder.java | 1 - .../fluent/agentic/spi/AgentDoFluent.java | 47 ++- .../fluent/agentic/Agents.java | 5 +- .../fluent/agentic/ChatBotIT.java | 354 +++++++++--------- .../fluent/func/FuncListenTaskBuilder.java | 2 - .../fluent/func/spi/CallFnFluent.java | 26 +- .../fluent/func/spi/FuncDoFluent.java | 5 +- .../fluent/spec/DoTaskBuilder.java | 3 +- .../fluent/spec/ExportBuilder.java | 66 ++-- .../fluent/spec/ListenTaskBuilder.java | 6 +- .../spec/SubscriptionIteratorBuilder.java | 96 ++--- .../fluent/spec/TaskItemListBuilder.java | 6 +- .../fluent/spec/spi/ForEachTaskFluent.java | 11 +- .../fluent/spec/spi/IteratorFluent.java | 41 +- .../fluent/spec/spi/OutputFluent.java | 25 +- .../spec/spi/SubscriptionIteratorFluent.java | 6 +- 22 files changed, 462 insertions(+), 390 deletions(-) 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 ca47c681..c6b41df6 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 @@ -15,15 +15,10 @@ */ 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 dev.langchain4j.agentic.scope.ResultWithAgenticScope; -import io.cloudevents.CloudEvent; -import io.cloudevents.CloudEventData; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.expressions.func.JavaModelCollection; -import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Optional; 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 63ca9709..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 @@ -30,7 +30,8 @@ class AgenticModelFactory implements WorkflowModelFactory { static final String DEFAULT_AGENTIC_SCOPE_STATE_KEY = "input"; private final AgenticScopeRegistryAssessor scopeRegistryAssessor = new AgenticScopeRegistryAssessor(); - private final AgenticScopeCloudEventsHandler scopeCloudEventsHandler = new AgenticScopeCloudEventsHandler(); + private final AgenticScopeCloudEventsHandler scopeCloudEventsHandler = + new AgenticScopeCloudEventsHandler(); @SuppressWarnings("unchecked") private AgenticModel newAgenticModel(Object state) { @@ -67,7 +68,8 @@ public WorkflowModel combine(Map workflowVariables) { @Override public WorkflowModelCollection createCollection() { - return new AgenticModelCollection(this.scopeRegistryAssessor.getAgenticScope(), scopeCloudEventsHandler); + return new AgenticModelCollection( + this.scopeRegistryAssessor.getAgenticScope(), scopeCloudEventsHandler); } @Override 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 index 39586093..8e9347eb 100644 --- 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 @@ -1,57 +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 java.io.IOException; -import java.util.Map; - 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(); + private final ObjectMapper mapper = new ObjectMapper(); - AgenticScopeCloudEventsHandler() {} + AgenticScopeCloudEventsHandler() {} - public void writeState(final AgenticScope scope, final CloudEvent cloudEvent) { - if (cloudEvent != null) { - writeState(scope, cloudEvent.getData()); - } + 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 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 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 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(); + 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 05620b64..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 @@ -19,7 +19,6 @@ 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; 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 cea7b9d4..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 @@ -15,13 +15,11 @@ */ package io.serverlessworkflow.fluent.agentic; -import dev.langchain4j.agentic.Agent; import io.serverlessworkflow.fluent.agentic.spi.AgentDoFluent; 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.FuncListenTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSetTaskBuilder; import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; 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 index ed38ee96..5a9b9359 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java @@ -1,34 +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 java.util.function.Predicate; - 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; + private UntilPredicate untilPredicate; - public AgentListenTaskBuilder() { - super(new AgentTaskItemListBuilder()); - } + public AgentListenTaskBuilder() { + super(new AgentTaskItemListBuilder()); + } - public AgentListenTaskBuilder until(Predicate predicate, Class predClass) { - untilPredicate = new UntilPredicate().withPredicate(predicate, predClass); - return this; - } + 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; + @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 aff81e60..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 @@ -24,7 +24,6 @@ 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.func.FuncTaskItemListBuilder; 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 89b70a58..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,9 +15,6 @@ */ package io.serverlessworkflow.fluent.agentic.spi; -import java.util.UUID; -import java.util.function.Consumer; - import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; import io.serverlessworkflow.fluent.agentic.LoopAgentsBuilder; import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; @@ -33,9 +30,11 @@ 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 SetFluent, + extends SetFluent, EmitFluent, ForEachFluent, SwitchFluent, @@ -43,33 +42,33 @@ public interface AgentDoFluent> ListenFluent, CallFnFluent { - SELF agent(String name, Object agent); + SELF agent(String name, Object agent); - default SELF agent(Object agent) { - return agent(UUID.randomUUID().toString(), agent); - } + default SELF agent(Object agent) { + return agent(UUID.randomUUID().toString(), agent); + } - SELF sequence(String name, Object... agents); + SELF sequence(String name, Object... agents); - default SELF sequence(Object... agents) { - return sequence("seq-" + UUID.randomUUID(), agents); - } + default SELF sequence(Object... agents) { + return sequence("seq-" + UUID.randomUUID(), agents); + } - SELF loop(String name, Consumer builder); + SELF loop(String name, Consumer builder); - default SELF loop(Consumer builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } + default SELF loop(Consumer builder) { + return loop("loop-" + UUID.randomUUID(), builder); + } - SELF loop(String name, LoopAgentsBuilder builder); + SELF loop(String name, LoopAgentsBuilder builder); - default SELF loop(LoopAgentsBuilder builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } + default SELF loop(LoopAgentsBuilder builder) { + return loop("loop-" + UUID.randomUUID(), builder); + } - SELF parallel(String name, Object... agents); + SELF parallel(String name, Object... agents); - default SELF parallel(Object... agents) { - return parallel("par-" + UUID.randomUUID(), agents); - } + default SELF parallel(Object... agents) { + return parallel("par-" + UUID.randomUUID(), agents); + } } 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 5e22b8f4..2172f449 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 @@ -17,7 +17,6 @@ import dev.langchain4j.agentic.Agent; import dev.langchain4j.agentic.internal.AgentSpecification; -import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; @@ -28,10 +27,10 @@ interface ChatBot { @UserMessage( """ You are a happy chat bot, reply to my message: - {message}. + {userInput}. """) @Agent - String chat(@V("message") String message); + String chat(@V("userInput") String userInput); } interface MovieExpert { 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 index ada4c978..8dd77f7a 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -15,18 +15,13 @@ */ package io.serverlessworkflow.fluent.agentic; -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; +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; @@ -35,174 +30,191 @@ import io.cloudevents.core.v1.CloudEventBuilder; import io.serverlessworkflow.api.types.EventFilter; import io.serverlessworkflow.api.types.EventProperties; -import io.serverlessworkflow.api.types.ListenTaskConfiguration; 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 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 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("message") - .build()); - BlockingQueue publishedEvents = 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.until(message -> "".equals(message.get("message")), Map.class) - .any(c -> c.with(event -> event.type("org.acme.chatbot.request"))) - .forEach(f -> f.tasks(tasks -> tasks + @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 publishedEvents = 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.until(message -> "".equals(message.get("userInput")), Map.class) + .any( + c -> + c.with(event -> event.type("org.acme.chatbot.request"))) + .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 -> publishedEvents.add((CloudEvent) ce)); - - app.eventConsumer().register(app.eventConsumer() - .listen( - new EventFilter() - .withWith(new EventProperties().withType("org.acme.chatbot.finished")), - app), - ce -> publishedEvents.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 event - app.eventPublisher().publish(newMessageEvent("Hello World!")); - CloudEvent reply = publishedEvents.poll(60, TimeUnit.SECONDS); - assertNotNull(reply); - - // Empty message completes the workflow - app.eventPublisher().publish(newMessageEvent("")); - CloudEvent finished = publishedEvents.poll(60, TimeUnit.SECONDS); - assertNotNull(finished); - - assertThat(runningModel).isCompleted(); - assertEquals(WorkflowStatus.COMPLETED, waitingInstance.status()); - - } catch (InterruptedException e) { - fail(e.getMessage()); - } + .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 -> publishedEvents.add((CloudEvent) ce)); + + app.eventConsumer() + .register( + app.eventConsumer() + .listen( + new EventFilter() + .withWith(new EventProperties().withType("org.acme.chatbot.finished")), + app), + ce -> publishedEvents.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 event + app.eventPublisher().publish(newMessageEvent("Hello World!")); + CloudEvent reply = publishedEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(reply); + + // Empty message completes the workflow + app.eventPublisher().publish(newMessageEvent("")); + CloudEvent finished = publishedEvents.poll(60, TimeUnit.SECONDS); + assertNotNull(finished); + + 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("message") - .build()); - - final Workflow mixedWorkflow = - AgentWorkflowBuilder.workflow("chat-bot") - .tasks( - t -> - t.callFn( - callJ -> - callJ.function( - input -> { - System.out.println(input); - return Map.of("message", 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 new CloudEventBuilder() - .withData(String.format("{\"message\": \"%s\"}", message).getBytes()) - .withType("org.acme.chatbot.request") - .withId(UUID.randomUUID().toString()) - .withDataContentType("application/json") - .withSource(URI.create("test://localhost")) - .withSubject("A chatbot message") - .withTime(OffsetDateTime.now()) - .build(); + } + + /** + * 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("message") + .build()); + + final Workflow mixedWorkflow = + AgentWorkflowBuilder.workflow("chat-bot") + .tasks( + t -> + t.callFn( + callJ -> + callJ.function( + input -> { + System.out.println(input); + return Map.of("message", 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 new CloudEventBuilder() + .withData(String.format("{\"userInput\": \"%s\"}", message).getBytes()) + .withType("org.acme.chatbot.request") + .withId(UUID.randomUUID().toString()) + .withDataContentType("application/json") + .withSource(URI.create("test://localhost")) + .withSubject("A chatbot message") + .withTime(OffsetDateTime.now()) + .build(); + } } 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 index 6ad28669..8b5a57fc 100644 --- a/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java +++ b/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncListenTaskBuilder.java @@ -20,8 +20,6 @@ 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.BaseDoTaskBuilder; -import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; import io.serverlessworkflow.fluent.spec.ListenTaskBuilder; import java.util.function.Predicate; 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 index 21b07585..f576601b 100644 --- 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 @@ -1,15 +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; -import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; - public interface CallFnFluent, LIST> { - LIST callFn(String name, Consumer cfg); + LIST callFn(String name, Consumer cfg); - default LIST callFn(Consumer cfg) { - return this.callFn(UUID.randomUUID().toString(), 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 eb46de9b..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 @@ -32,11 +32,10 @@ // TODO: implement the other builders, e.g. CallHTTP public interface FuncDoFluent> - extends SetFluent, + extends SetFluent, EmitFluent, ForEachFluent, SwitchFluent, ForkFluent, ListenFluent, - CallFnFluent { -} + CallFnFluent {} 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 38c383fc..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/ExportBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java index 095a61a8..a5f600c7 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ExportBuilder.java @@ -24,43 +24,43 @@ import io.serverlessworkflow.api.types.SchemaUnion; public final class ExportBuilder { - private final Export export; + private final Export export; - public ExportBuilder() { - this.export = new Export(); - this.export.setAs(new ExportAs()); - this.export.setSchema(new SchemaUnion()); - } + 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(Object as) { + this.export.getAs().withObject(as); + return this; + } - public ExportBuilder as(String as) { - this.export.getAs().withString(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(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 ExportBuilder schema(Object schema) { + this.export.getSchema().setSchemaInline(new SchemaInline(schema)); + return this; + } - public Export build() { - return this.export; - } + 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 b5bfd74f..5633cf2a 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 @@ -31,7 +31,8 @@ * 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; @@ -90,7 +91,8 @@ public ListenTaskBuilder any(Consumer c) { } public ListenTaskBuilder forEach(Consumer> c) { - final SubscriptionIteratorBuilder iteratorBuilder = new SubscriptionIteratorBuilder<>(this.taskItemListBuilder); + final SubscriptionIteratorBuilder iteratorBuilder = + new SubscriptionIteratorBuilder<>(this.taskItemListBuilder); c.accept(iteratorBuilder); this.listenTask.setForeach(iteratorBuilder.build()); return this; 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 index 1f5ca0a5..0c9d509b 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/SubscriptionIteratorBuilder.java @@ -15,64 +15,64 @@ */ package io.serverlessworkflow.fluent.spec; -import java.util.function.Consumer; - import io.serverlessworkflow.api.types.SubscriptionIterator; import io.serverlessworkflow.fluent.spec.spi.SubscriptionIteratorFluent; +import java.util.function.Consumer; -public class SubscriptionIteratorBuilder> implements SubscriptionIteratorFluent, T> { +public class SubscriptionIteratorBuilder> + implements SubscriptionIteratorFluent, T> { - private final SubscriptionIterator subscriptionIterator; - private final T taskItemListBuilder; + private final SubscriptionIterator subscriptionIterator; + private final T taskItemListBuilder; - public SubscriptionIteratorBuilder(T taskItemListBuilder) { - subscriptionIterator = new SubscriptionIterator(); - this.taskItemListBuilder = 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 item(String item) { + subscriptionIterator.setItem(item); + return this; + } - @Override - public SubscriptionIteratorBuilder at(String at) { - subscriptionIterator.setAt(at); - 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 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 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 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; - } + @Override + public SubscriptionIteratorBuilder exportAs(Object exportAs) { + this.subscriptionIterator.setExport(new ExportBuilder().as(exportAs).build()); + return this; + } - public SubscriptionIterator build() { - return subscriptionIterator; - } + public SubscriptionIterator build() { + return subscriptionIterator; + } } 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 3edfc368..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<>(newItemListBuilder()); + 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/ForEachTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/ForEachTaskFluent.java index c233d7e1..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 @@ -20,13 +20,14 @@ import io.serverlessworkflow.fluent.spec.TaskBaseBuilder; public interface ForEachTaskFluent< - SELF extends TaskBaseBuilder, L extends BaseTaskItemListBuilder> extends IteratorFluent { + SELF extends TaskBaseBuilder, L extends BaseTaskItemListBuilder> + extends IteratorFluent { - SELF each(String each); + SELF each(String each); - SELF in(String in); + SELF in(String in); - SELF whileC(final String expression); + SELF whileC(final String expression); - ForTask build(); + 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 index abfe003b..85c61777 100644 --- 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 @@ -1,22 +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 java.util.function.Consumer; - 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); + /** + * 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/OutputFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/OutputFluent.java index 2a13399a..cc49a43c 100644 --- 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 @@ -1,16 +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 java.util.function.Consumer; - 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 output(Consumer outputConsumer); - SELF exportAs(Object exportAs); + 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 index 197fd6f7..8323833a 100644 --- 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 @@ -17,8 +17,8 @@ import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; -public interface SubscriptionIteratorFluent > extends IteratorFluent, OutputFluent { - - SELF item(String item); +public interface SubscriptionIteratorFluent> + extends IteratorFluent, OutputFluent { + SELF item(String item); } From a60c1cc3136909184638d4b43c4624cacbba6809 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 18 Aug 2025 18:58:06 -0400 Subject: [PATCH 8/9] Adjusting the chatBotIT to add when handlers to emit Signed-off-by: Ricardo Zanini --- .../agentic/AgenticModelCollection.java | 2 ++ .../fluent/agentic/Agents.java | 4 +-- .../fluent/agentic/ChatBotIT.java | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) 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 c6b41df6..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 @@ -58,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/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 2172f449..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 @@ -27,7 +27,7 @@ interface ChatBot { @UserMessage( """ You are a happy chat bot, reply to my message: - {userInput}. + {{userInput}}. """) @Agent String chat(@V("userInput") String userInput); @@ -39,7 +39,7 @@ interface MovieExpert { """ 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 index 8dd77f7a..4d3f3d4e 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -60,7 +60,8 @@ void chat_bot() { // .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) .outputName("conversation") .build()); - BlockingQueue publishedEvents = new LinkedBlockingQueue<>(); + 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 @@ -71,7 +72,13 @@ void chat_bot() { t -> t.listen( l -> - l.until(message -> "".equals(message.get("userInput")), Map.class) + l.until( + message -> + !message + .getOrDefault("userInput", "") + .toString() + .isEmpty(), + Map.class) .any( c -> c.with(event -> event.type("org.acme.chatbot.request"))) @@ -87,7 +94,16 @@ void chat_bot() { e -> e.type( "org.acme.chatbot.reply")))))) - .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.finished")))) + .emit( + emit -> + emit.when( + message -> + message + .getOrDefault("userInput", "") + .toString() + .isEmpty(), + Map.class) + .event(e -> e.type("org.acme.chatbot.finished")))) .build(); try (WorkflowApplication app = WorkflowApplication.builder().build()) { @@ -98,7 +114,7 @@ void chat_bot() { new EventFilter() .withWith(new EventProperties().withType("org.acme.chatbot.reply")), app), - ce -> publishedEvents.add((CloudEvent) ce)); + ce -> replyEvents.add((CloudEvent) ce)); app.eventConsumer() .register( @@ -107,7 +123,7 @@ void chat_bot() { new EventFilter() .withWith(new EventProperties().withType("org.acme.chatbot.finished")), app), - ce -> publishedEvents.add((CloudEvent) ce)); + ce -> finishedEvents.add((CloudEvent) ce)); final WorkflowInstance waitingInstance = app.workflowDefinition(listenWorkflow).instance(Map.of()); @@ -118,12 +134,12 @@ void chat_bot() { // Publish the event app.eventPublisher().publish(newMessageEvent("Hello World!")); - CloudEvent reply = publishedEvents.poll(60, TimeUnit.SECONDS); + CloudEvent reply = replyEvents.poll(60, TimeUnit.SECONDS); assertNotNull(reply); // Empty message completes the workflow app.eventPublisher().publish(newMessageEvent("")); - CloudEvent finished = publishedEvents.poll(60, TimeUnit.SECONDS); + CloudEvent finished = finishedEvents.poll(60, TimeUnit.SECONDS); assertNotNull(finished); assertThat(runningModel).isCompleted(); @@ -158,7 +174,7 @@ void mixed_workflow() { AgenticServices.agentBuilder(Agents.ChatBot.class) .chatModel(Models.BASE_MODEL) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) - .outputName("message") + .outputName("userInput") .build()); final Workflow mixedWorkflow = @@ -170,7 +186,7 @@ void mixed_workflow() { callJ.function( input -> { System.out.println(input); - return Map.of("message", input); + return Map.of("userInput", input); }, String.class)) .agent(chatBot) From dd8e6e1d20b6a931734737c92ad0a41a1f35bae4 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 19 Aug 2025 15:18:49 -0400 Subject: [PATCH 9/9] Refactor workflow to run until receive a finalized message Signed-off-by: Ricardo Zanini --- .../fluent/agentic/ChatBotIT.java | 52 ++++---- ...stractEventConsumptionStrategyBuilder.java | 114 ++++++++++++++++++ .../spec/EventConsumptionStrategyBuilder.java | 56 +++++++++ .../fluent/spec/EventFilterBuilder.java | 49 ++++++++ .../fluent/spec/ListenTaskBuilder.java | 84 +------------ .../fluent/spec/ListenToBuilder.java | 55 +++++++++ .../spi/EventConsumptionStrategyFluent.java | 37 ++++++ .../fluent/spec/WorkflowBuilderTest.java | 8 +- 8 files changed, 352 insertions(+), 103 deletions(-) create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/AbstractEventConsumptionStrategyBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventConsumptionStrategyBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/EventFilterBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenToBuilder.java create mode 100644 fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/EventConsumptionStrategyFluent.java 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 index 4d3f3d4e..397f2183 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java @@ -72,16 +72,22 @@ void chat_bot() { t -> t.listen( l -> - l.until( - message -> - !message - .getOrDefault("userInput", "") - .toString() - .isEmpty(), - Map.class) - .any( - c -> - c.with(event -> event.type("org.acme.chatbot.request"))) + 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( @@ -94,16 +100,7 @@ void chat_bot() { e -> e.type( "org.acme.chatbot.reply")))))) - .emit( - emit -> - emit.when( - message -> - message - .getOrDefault("userInput", "") - .toString() - .isEmpty(), - Map.class) - .event(e -> e.type("org.acme.chatbot.finished")))) + .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.finished")))) .build(); try (WorkflowApplication app = WorkflowApplication.builder().build()) { @@ -132,15 +129,16 @@ void chat_bot() { // The workflow is just waiting for the event assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); - // Publish the event + // 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("")); + 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()); @@ -223,9 +221,17 @@ void mixed_workflow() { } 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("org.acme.chatbot.request") + .withType(type) .withId(UUID.randomUUID().toString()) .withDataContentType("application/json") .withSource(URI.create("test://localhost")) 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/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/ListenTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/ListenTaskBuilder.java index 5633cf2a..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,16 +15,10 @@ */ 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; /** @@ -36,7 +30,6 @@ public class ListenTaskBuilder> private final ListenTask listenTask; private final ListenTaskConfiguration config; - private boolean oneSet, allSet, anySet; private final T taskItemListBuilder; public ListenTaskBuilder(T taskItemListBuilder) { @@ -54,42 +47,6 @@ 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); - 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); - 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); - return this; - } - public ListenTaskBuilder forEach(Consumer> c) { final SubscriptionIteratorBuilder iteratorBuilder = new SubscriptionIteratorBuilder<>(this.taskItemListBuilder); @@ -103,48 +60,17 @@ public ListenTaskBuilder read(ListenTaskConfiguration.ListenAndReadAs listenA return this; } - private void ensureNoneSet() { - if (oneSet || allSet || anySet) { - throw new IllegalStateException("Only one consumption strategy can be configured"); - } + public ListenTaskBuilder to(Consumer c) { + final ListenToBuilder listenToBuilder = new ListenToBuilder(); + c.accept(listenToBuilder); + this.config.setTo(listenToBuilder.build()); + return this; } - /** 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/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/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();