From 1a0bcef620e0be7600b3027a552846724f61b364 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 9 Dec 2025 12:12:53 -0500 Subject: [PATCH] NO-ISSUE: Remove LangChain4j integration Signed-off-by: Ricardo Zanini --- experimental/agentic/pom.xml | 49 --- .../impl/model/agentic/AgenticModel.java | 59 --- .../model/agentic/AgenticModelCollection.java | 67 --- .../model/agentic/AgenticModelFactory.java | 136 ------ .../AgenticScopeCloudEventsHandler.java | 70 --- .../AgenticScopeRegistryAssessor.java | 85 ---- ...rverlessworkflow.impl.WorkflowModelFactory | 1 - .../fluent/agentic-langchain4j/pom.xml | 61 --- .../langchain4j/AbstractAgentService.java | 105 ----- .../ConditionalAgentServiceImpl.java | 82 ---- .../langchain4j/LC4JWorkflowBuilder.java | 66 --- .../langchain4j/LoopAgentServiceImpl.java | 91 ---- .../langchain4j/ParallelAgentServiceImpl.java | 73 ---- .../SequentialAgentServiceImpl.java | 60 --- .../WorkflowDefinitionBuilder.java | 25 -- .../WorkflowInvocationHandler.java | 180 -------- .../fluent/agentic/langchain4j/Agents.java | 62 --- .../fluent/agentic/langchain4j/Models.java | 49 --- .../SequentialAgentServiceImplTest.java | 119 ----- .../agentic/langchain4j/WorkflowAgentsIT.java | 80 ---- experimental/fluent/agentic/README.md | 179 -------- experimental/fluent/agentic/pom.xml | 87 ---- .../fluent/agentic/AgentAdapters.java | 49 --- .../fluent/agentic/AgentDoTaskBuilder.java | 120 ------ .../agentic/AgentListenTaskBuilder.java | 61 --- .../agentic/AgentTaskItemListBuilder.java | 157 ------- .../fluent/agentic/AgentWorkflowBuilder.java | 118 ----- .../fluent/agentic/AgenticWorkflow.java | 134 ------ .../fluent/agentic/LoopAgentsBuilder.java | 81 ---- .../configurers/AgentListenConfigurer.java | 22 - .../configurers/AgentTaskConfigurer.java | 22 - .../fluent/agentic/dsl/AgentListenSpec.java | 40 -- .../fluent/agentic/dsl/AgenticDSL.java | 169 -------- .../fluent/agentic/spi/AgentDoFluent.java | 74 ---- .../fluent/agentic/AgentDslWorkflowTest.java | 142 ------ .../agentic/AgentTaskItemListBuilderTest.java | 86 ---- .../agentic/AgentWorkflowBuilderTest.java | 223 ---------- .../agentic/AgenticWorkflowHelperIT.java | 225 ---------- .../fluent/agentic/Agents.java | 408 ------------------ .../fluent/agentic/AgentsUtils.java | 140 ------ .../fluent/agentic/ChatBotIT.java | 171 -------- .../agentic/CloudEventsTestBuilder.java | 42 -- .../fluent/agentic/EmailDrafterIT.java | 141 ------ .../fluent/agentic/MixedWorkflowIT.java | 93 ---- .../fluent/agentic/Models.java | 32 -- .../fluent/agentic/README.md | 406 ----------------- .../fluent/agentic/WorkflowTests.java | 319 -------------- .../src/test/java/org/acme/EmailDraft.java | 20 - .../src/test/java/org/acme/EmailDrafts.java | 38 -- .../src/test/java/org/acme/EmailPolicies.java | 69 --- .../test/java/org/acme/PolicyDecision.java | 21 - .../fluent/func/FuncForkTaskBuilder.java | 1 + experimental/fluent/pom.xml | 10 +- experimental/pom.xml | 1 - pom.xml | 9 - 55 files changed, 3 insertions(+), 5427 deletions(-) delete mode 100644 experimental/agentic/pom.xml delete mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModel.java delete mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelCollection.java delete mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelFactory.java delete mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticScopeCloudEventsHandler.java delete mode 100644 experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/langchain4j/AgenticScopeRegistryAssessor.java delete mode 100644 experimental/agentic/src/main/resources/META-INF/services/io.serverlessworkflow.impl.WorkflowModelFactory delete mode 100644 experimental/fluent/agentic-langchain4j/pom.xml delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java delete mode 100644 experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java delete mode 100644 experimental/fluent/agentic/README.md delete mode 100644 experimental/fluent/agentic/pom.xml delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflow.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java delete mode 100644 experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentDslWorkflowTest.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/CloudEventsTestBuilder.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/MixedWorkflowIT.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md delete mode 100644 experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java delete mode 100644 experimental/fluent/agentic/src/test/java/org/acme/EmailDraft.java delete mode 100644 experimental/fluent/agentic/src/test/java/org/acme/EmailDrafts.java delete mode 100644 experimental/fluent/agentic/src/test/java/org/acme/EmailPolicies.java delete mode 100644 experimental/fluent/agentic/src/test/java/org/acme/PolicyDecision.java diff --git a/experimental/agentic/pom.xml b/experimental/agentic/pom.xml deleted file mode 100644 index a6ea803e9..000000000 --- a/experimental/agentic/pom.xml +++ /dev/null @@ -1,49 +0,0 @@ - - 4.0.0 - - io.serverlessworkflow - serverlessworkflow-experimental - 8.0.0-SNAPSHOT - - serverlessworkflow-experimental-agentic - Serverless Workflow :: Experimental :: Agentic - - - io.serverlessworkflow - serverlessworkflow-experimental-lambda - - - io.serverlessworkflow - serverlessworkflow-experimental-model - - - dev.langchain4j - langchain4j-agentic - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.assertj - assertj-core - test - - - ch.qos.logback - logback-classic - test - - - \ No newline at end of file diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModel.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModel.java deleted file mode 100644 index 3046047a7..000000000 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModel.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.model.agentic; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.impl.WorkflowModel; -import io.serverlessworkflow.impl.model.func.JavaModel; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; - -class AgenticModel extends JavaModel { - - private final AgenticScope agenticScope; - - AgenticModel(AgenticScope agenticScope, Object object) { - super(object); - this.agenticScope = agenticScope; - } - - public AgenticScope getAgenticScope() { - return agenticScope; - } - - @Override - public Collection asCollection() { - throw new UnsupportedOperationException("asCollection() is not supported yet."); - } - - @Override - public Optional> asMap() { - return Optional.of(this.agenticScope.state()); - } - - @Override - public Object asJavaObject() { - return agenticScope; - } - - @Override - protected Optional convert(Class clazz) { - return AgenticScope.class.isAssignableFrom(clazz) - ? Optional.of(clazz.cast(this.agenticScope)) - : super.convert(clazz); - } -} diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelCollection.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelCollection.java deleted file mode 100644 index a27866ab1..000000000 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelCollection.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.model.agentic; - -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.scope.ResultWithAgenticScope; -import io.serverlessworkflow.impl.WorkflowModel; -import io.serverlessworkflow.impl.model.func.JavaModelCollection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; - -public class AgenticModelCollection extends JavaModelCollection { - - private final AgenticScope agenticScope; - private final AgenticScopeCloudEventsHandler ceHandler; - - AgenticModelCollection(AgenticScope agenticScope, AgenticScopeCloudEventsHandler ceHandler) { - super(Collections.emptyList()); - this.agenticScope = agenticScope; - this.ceHandler = ceHandler; - } - - @Override - public boolean add(WorkflowModel e) { - Optional> asMap = e.asMap(); - if (asMap.isPresent() && !asMap.get().isEmpty()) { - this.agenticScope.writeStates(asMap.get()); - return super.add(e); - } - - // Update the agenticScope with the event body, so agents can use the event data as input - Object value = e.asJavaObject(); - if (!ceHandler.writeStateIfCloudEvent(this.agenticScope, value)) { - this.agenticScope.writeState(AgenticModelFactory.DEFAULT_AGENTIC_SCOPE_STATE_KEY, value); - } - - // add to the collection - return super.add(e); - } - - @Override - public Optional as(Class clazz) { - if (AgenticScope.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(agenticScope)); - } else if (ResultWithAgenticScope.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(new ResultWithAgenticScope<>(agenticScope, object))); - } else if (Map.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(agenticScope.state())); - } else { - return super.as(clazz); - } - } -} diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelFactory.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelFactory.java deleted file mode 100644 index f14c54ea7..000000000 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticModelFactory.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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.model.agentic; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.cloudevents.CloudEvent; -import io.cloudevents.CloudEventData; -import io.serverlessworkflow.impl.WorkflowModel; -import io.serverlessworkflow.impl.WorkflowModelCollection; -import io.serverlessworkflow.impl.WorkflowModelFactory; -import io.serverlessworkflow.impl.model.agentic.langchain4j.AgenticScopeRegistryAssessor; -import java.time.OffsetDateTime; -import java.util.HashMap; -import java.util.Map; - -public 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(); - - @SuppressWarnings("unchecked") - private AgenticModel newAgenticModel(Object state) { - if (state == null) { - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), null); - } - - if (state instanceof Map) { - this.scopeRegistryAssessor.writeStates((Map) state); - } else { - this.scopeRegistryAssessor.writeState(DEFAULT_AGENTIC_SCOPE_STATE_KEY, state); - } - - return new AgenticModel(this.scopeRegistryAssessor.getAgenticScope(), state); - } - - @Override - public WorkflowModel fromAny(WorkflowModel prev, Object obj) { - // 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()); - agenticModel.getAgenticScope().state().put(DEFAULT_AGENTIC_SCOPE_STATE_KEY, obj); - } - return newAgenticModel(obj); - } - - @Override - public WorkflowModel combine(Map workflowVariables) { - Map map = new HashMap<>(); - for (Map.Entry e : workflowVariables.entrySet()) { - if (e.getValue() instanceof AgenticModel agenticModel) { - map.putAll(agenticModel.getAgenticScope().state()); - } else { - map.put(e.getKey(), e.getValue().asJavaObject()); - } - } - return newAgenticModel(map); - } - - @Override - public WorkflowModelCollection createCollection() { - return new AgenticModelCollection( - this.scopeRegistryAssessor.getAgenticScope(), scopeCloudEventsHandler); - } - - @Override - public WorkflowModel from(boolean value) { - return newAgenticModel(value); - } - - @Override - public WorkflowModel from(Number value) { - return newAgenticModel(value); - } - - @Override - public WorkflowModel from(String value) { - return newAgenticModel(value); - } - - @Override - public WorkflowModel from(CloudEvent ce) { - return from(scopeCloudEventsHandler.extractDataAsMap(ce)); - } - - @Override - public WorkflowModel from(CloudEventData ce) { - return from(scopeCloudEventsHandler.extractDataAsMap(ce)); - } - - @Override - public WorkflowModel from(OffsetDateTime value) { - return newAgenticModel(value); - } - - @Override - public WorkflowModel from(Map map) { - return newAgenticModel(map); - } - - @Override - public WorkflowModel fromNull() { - return newAgenticModel(null); - } - - @Override - public WorkflowModel fromOther(Object value) { - if (value instanceof AgenticScope scope) { - return new AgenticModel(scope, scope.state()); - } - return newAgenticModel(value); - } - - public AgenticModelFactory() {} - - @Override - public int priority() { - return DEFAULT_PRIORITY - 1; - } -} diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticScopeCloudEventsHandler.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticScopeCloudEventsHandler.java deleted file mode 100644 index 1ee807fed..000000000 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/AgenticScopeCloudEventsHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.model.agentic; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import dev.langchain4j.agentic.scope.AgenticScope; -import io.cloudevents.CloudEvent; -import io.cloudevents.CloudEventData; -import java.io.IOException; -import java.util.Map; - -public final class AgenticScopeCloudEventsHandler { - - private final ObjectMapper mapper = new ObjectMapper(); - - AgenticScopeCloudEventsHandler() {} - - public void writeState(final AgenticScope scope, final CloudEvent cloudEvent) { - if (cloudEvent != null) { - writeState(scope, cloudEvent.getData()); - } - } - - public void writeState(final AgenticScope scope, final CloudEventData cloudEvent) { - scope.writeStates(extractDataAsMap(cloudEvent)); - } - - public boolean writeStateIfCloudEvent(final AgenticScope scope, final Object value) { - if (value instanceof CloudEvent) { - writeState(scope, (CloudEvent) value); - return true; - } else if (value instanceof CloudEventData) { - writeState(scope, (CloudEventData) value); - return true; - } - return false; - } - - public Map extractDataAsMap(final CloudEventData ce) { - try { - if (ce != null) { - return mapper.readValue(ce.toBytes(), new TypeReference<>() {}); - } - } catch (IOException e) { - throw new IllegalArgumentException("Unable to parse CloudEvent data as JSON", e); - } - return Map.of(); - } - - public Map extractDataAsMap(final CloudEvent ce) { - if (ce != null) { - return extractDataAsMap(ce.getData()); - } - return Map.of(); - } -} diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/langchain4j/AgenticScopeRegistryAssessor.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/langchain4j/AgenticScopeRegistryAssessor.java deleted file mode 100644 index 68f057e9f..000000000 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/model/agentic/langchain4j/AgenticScopeRegistryAssessor.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.model.agentic.langchain4j; - -import dev.langchain4j.agentic.internal.AgenticScopeOwner; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.scope.AgenticScopeRegistry; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; -import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; - -public class AgenticScopeRegistryAssessor implements AgenticScopeOwner { - - private final AtomicReference agenticScopeRegistry = - new AtomicReference<>(); - private final String agentId; - private AgenticScope agenticScope; - private Object memoryId; - - public AgenticScopeRegistryAssessor(String agentId) { - Objects.requireNonNull(agentId, "Agent id cannot be null"); - this.agentId = agentId; - } - - // TODO: have access to the workflow definition and assign its name instead - public AgenticScopeRegistryAssessor() { - this.agentId = UUID.randomUUID().toString(); - } - - public void setMemoryId(Object memoryId) { - this.memoryId = memoryId; - } - - public AgenticScope getAgenticScope() { - if (agenticScope != null) { - return agenticScope; - } - - if (memoryId != null) { - this.agenticScope = registry().getOrCreate(memoryId); - } else { - this.agenticScope = registry().createEphemeralAgenticScope(); - } - return this.agenticScope; - } - - public void setAgenticScope(AgenticScope agenticScope) { - this.agenticScope = Objects.requireNonNull(agenticScope, "AgenticScope cannot be null"); - } - - public void writeState(String key, Object value) { - this.getAgenticScope().writeState(key, value); - } - - public void writeStates(Map states) { - this.getAgenticScope().writeStates(states); - } - - @Override - public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { - this.setAgenticScope(agenticScope); - return this; - } - - @Override - public AgenticScopeRegistry registry() { - agenticScopeRegistry.compareAndSet(null, new AgenticScopeRegistry(agentId)); - return agenticScopeRegistry.get(); - } -} diff --git a/experimental/agentic/src/main/resources/META-INF/services/io.serverlessworkflow.impl.WorkflowModelFactory b/experimental/agentic/src/main/resources/META-INF/services/io.serverlessworkflow.impl.WorkflowModelFactory deleted file mode 100644 index eac09c0f2..000000000 --- a/experimental/agentic/src/main/resources/META-INF/services/io.serverlessworkflow.impl.WorkflowModelFactory +++ /dev/null @@ -1 +0,0 @@ -io.serverlessworkflow.impl.model.agentic.AgenticModelFactory \ No newline at end of file diff --git a/experimental/fluent/agentic-langchain4j/pom.xml b/experimental/fluent/agentic-langchain4j/pom.xml deleted file mode 100644 index 6f069cd8f..000000000 --- a/experimental/fluent/agentic-langchain4j/pom.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - 4.0.0 - - io.serverlessworkflow - serverlessworkflow-experimental-fluent - 8.0.0-SNAPSHOT - - - serverlessworkflow-experimental-fluent-agentic-langchain4j - Serverless Workflow :: Experimental :: Fluent :: Agentic LangChain4j - Agentic Workflow DSL Implementation for langchain4j-agentic - - - - io.serverlessworkflow - serverlessworkflow-experimental-agentic - - - io.serverlessworkflow - serverlessworkflow-experimental-fluent-agentic - - - dev.langchain4j - langchain4j-agentic - - - - org.slf4j - slf4j-simple - test - - - org.junit.jupiter - junit-jupiter-api - test - - - org.mockito - mockito-core - test - - - org.assertj - assertj-core - test - - - dev.langchain4j - langchain4j-ollama - test - ${version.dev.langchain4j} - - - io.serverlessworkflow - serverlessworkflow-experimental-fluent-agentic - test-jar - test - - - \ No newline at end of file diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java deleted file mode 100644 index 7f830cf2e..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.agent.ErrorContext; -import dev.langchain4j.agentic.agent.ErrorRecoveryResult; -import dev.langchain4j.agentic.internal.AgentSpecification; -import dev.langchain4j.agentic.internal.AgenticScopeOwner; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder; -import io.serverlessworkflow.impl.WorkflowApplication; -import java.lang.reflect.Proxy; -import java.util.function.Consumer; -import java.util.function.Function; - -public abstract class AbstractAgentService implements WorkflowDefinitionBuilder { - - // Workflow OutputAs - private static final Function DEFAULT_OUTPUT_FUNCTION = - agenticScope -> null; - - protected final WorkflowApplication.Builder workflowExecBuilder; - protected final AgentWorkflowBuilder workflowBuilder; - protected final Class agentServiceClass; - - protected AbstractAgentService(Class agentServiceClass) { - this.workflowBuilder = AgentWorkflowBuilder.workflow().outputAs(DEFAULT_OUTPUT_FUNCTION); - this.agentServiceClass = agentServiceClass; - this.workflowExecBuilder = WorkflowApplication.builder(); - } - - @SuppressWarnings("unchecked") - public T build() { - return (T) - Proxy.newProxyInstance( - this.agentServiceClass.getClassLoader(), - new Class[] {agentServiceClass, AgentSpecification.class, AgenticScopeOwner.class}, - new WorkflowInvocationHandler( - this.workflowBuilder.build(), this.workflowExecBuilder, this.agentServiceClass)); - } - - @SuppressWarnings("unchecked") - public S beforeCall(Consumer beforeCall) { - this.workflowBuilder.inputFrom( - cog -> { - beforeCall.accept(cog); - return cog; - }, - AgenticScope.class); - return (S) this; - } - - @SuppressWarnings("unchecked") - public S outputKey(String outputName) { - Function outputFunction = cog -> cog.readState(outputName); - this.workflowBuilder.outputAs(outputFunction, DefaultAgenticScope.class); - this.workflowBuilder.document( - d -> d.metadata(m -> m.metadata(META_KEY_OUTPUTNAME, outputName))); - return (S) this; - } - - @SuppressWarnings("unchecked") - public S output(Function output) { - this.workflowBuilder.outputAs(output, AgenticScope.class); - return (S) this; - } - - @SuppressWarnings("unchecked") - public S errorHandler(Function errorHandler) { - // TODO: implement - return (S) this; - } - - @Override - public Workflow getDefinition() { - return this.workflowBuilder.build(); - } - - @SuppressWarnings("unchecked") - public S name(String name) { - this.workflowBuilder.document(d -> d.name(name)); - return (S) this; - } - - @SuppressWarnings("unchecked") - public S description(String description) { - this.workflowBuilder.document(d -> d.summary(description)); - return (S) this; - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java deleted file mode 100644 index 0aff4aaff..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.agent.AgentRequest; -import dev.langchain4j.agentic.agent.AgentResponse; -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.workflow.ConditionalAgentService; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Predicate; - -public class ConditionalAgentServiceImpl - extends AbstractAgentService> - implements ConditionalAgentService { - - private ConditionalAgentServiceImpl(Class agentServiceClass) { - super(agentServiceClass); - } - - public static ConditionalAgentService builder(Class agentServiceClass) { - return new ConditionalAgentServiceImpl<>(agentServiceClass); - } - - @Override - public ConditionalAgentService subAgents(Object... agents) { - this.workflowBuilder.tasks(t -> t.sequence(agents)); - return this; - } - - @Override - public ConditionalAgentService subAgents(List agentExecutors) { - return this.subAgents(agentExecutors.toArray()); - } - - @Override - public ConditionalAgentService beforeAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public ConditionalAgentService afterAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public ConditionalAgentService subAgents(Predicate condition, Object... agents) { - this.workflowBuilder.tasks( - t -> Arrays.stream(agents).forEach(agent -> t.when(condition).agent(agent))); - return this; - } - - @Override - public ConditionalAgentService subAgents( - Predicate condition, List agentExecutors) { - return this.subAgents(condition, agentExecutors.toArray()); - } - - @Override - public ConditionalAgentService subAgent( - Predicate condition, AgentExecutor agentExecutor) { - this.workflowBuilder.tasks(t -> t.when(condition).agent(agentExecutor)); - return this; - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java deleted file mode 100644 index 7d5428be7..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.UntypedAgent; -import dev.langchain4j.agentic.workflow.ConditionalAgentService; -import dev.langchain4j.agentic.workflow.LoopAgentService; -import dev.langchain4j.agentic.workflow.ParallelAgentService; -import dev.langchain4j.agentic.workflow.SequentialAgentService; -import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder; - -public class LC4JWorkflowBuilder implements WorkflowAgentsBuilder { - - @Override - public SequentialAgentService sequenceBuilder() { - return SequentialAgentServiceImpl.builder(UntypedAgent.class); - } - - @Override - public SequentialAgentService sequenceBuilder(Class agentServiceClass) { - return SequentialAgentServiceImpl.builder(agentServiceClass); - } - - @Override - public ParallelAgentService parallelBuilder() { - return ParallelAgentServiceImpl.builder(UntypedAgent.class); - } - - @Override - public ParallelAgentService parallelBuilder(Class agentServiceClass) { - return ParallelAgentServiceImpl.builder(agentServiceClass); - } - - @Override - public LoopAgentService loopBuilder() { - return LoopAgentServiceImpl.builder(UntypedAgent.class); - } - - @Override - public LoopAgentService loopBuilder(Class agentServiceClass) { - return LoopAgentServiceImpl.builder(agentServiceClass); - } - - @Override - public ConditionalAgentService conditionalBuilder() { - return ConditionalAgentServiceImpl.builder(UntypedAgent.class); - } - - @Override - public ConditionalAgentService conditionalBuilder(Class agentServiceClass) { - return ConditionalAgentServiceImpl.builder(agentServiceClass); - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java deleted file mode 100644 index c4ac83283..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.agent.AgentRequest; -import dev.langchain4j.agentic.agent.AgentResponse; -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.workflow.LoopAgentService; -import io.serverlessworkflow.fluent.agentic.LoopAgentsBuilder; -import java.util.List; -import java.util.function.BiPredicate; -import java.util.function.Consumer; -import java.util.function.Predicate; - -public class LoopAgentServiceImpl extends AbstractAgentService> - implements LoopAgentService { - - private final LoopAgentsBuilder loopAgentsBuilder = new LoopAgentsBuilder(); - - private LoopAgentServiceImpl(Class agentServiceClass) { - super(agentServiceClass); - } - - public static LoopAgentService builder(Class agentServiceClass) { - return new LoopAgentServiceImpl<>(agentServiceClass); - } - - @Override - public LoopAgentService maxIterations(int maxIterations) { - this.loopAgentsBuilder.maxIterations(maxIterations); - return this; - } - - @Override - public LoopAgentService exitCondition(Predicate exitCondition) { - this.loopAgentsBuilder.exitCondition(exitCondition); - return this; - } - - @Override - public LoopAgentService exitCondition(BiPredicate biPredicate) { - this.loopAgentsBuilder.exitCondition(biPredicate); - return this; - } - - @Override - public LoopAgentService testExitAtLoopEnd(boolean b) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public LoopAgentService subAgents(Object... agents) { - this.loopAgentsBuilder.subAgents(agents); - this.workflowBuilder.tasks(t -> t.loop(this.loopAgentsBuilder)); - return this; - } - - @Override - public LoopAgentService subAgents(List agentExecutors) { - this.loopAgentsBuilder.subAgents(agentExecutors.toArray()); - this.workflowBuilder.tasks(t -> t.loop(this.loopAgentsBuilder)); - return this; - } - - @Override - public LoopAgentService beforeAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public LoopAgentService afterAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java deleted file mode 100644 index 607c06177..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.agent.AgentRequest; -import dev.langchain4j.agentic.agent.AgentResponse; -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.workflow.ParallelAgentService; -import io.serverlessworkflow.impl.ExecutorServiceHolder; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; - -public class ParallelAgentServiceImpl extends AbstractAgentService> - implements ParallelAgentService { - - private ParallelAgentServiceImpl(Class agentServiceClass) { - super(agentServiceClass); - } - - public static ParallelAgentService builder(Class agentServiceClass) { - return new ParallelAgentServiceImpl<>(agentServiceClass); - } - - @Override - public ParallelAgentService subAgents(Object... agents) { - this.workflowBuilder.tasks(t -> t.parallel(agents)); - return this; - } - - @Override - public ParallelAgentService subAgents(List agentExecutors) { - return this.subAgents(agentExecutors.toArray()); - } - - @Override - public ParallelAgentService beforeAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public ParallelAgentService afterAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public ParallelAgentService executor(Executor executor) { - if (!(executor instanceof ExecutorService)) { - throw new IllegalArgumentException( - "ExecutorService is required for the ParallelAgentService"); - } - // TODO: create an adapter or change or internal holder to accept a plain `Executor`. - this.workflowExecBuilder.withExecutorFactory( - new ExecutorServiceHolder((ExecutorService) executor)); - return this; - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java deleted file mode 100644 index 184dbb78a..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.agent.AgentRequest; -import dev.langchain4j.agentic.agent.AgentResponse; -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.workflow.SequentialAgentService; -import java.util.List; -import java.util.function.Consumer; - -public class SequentialAgentServiceImpl - extends AbstractAgentService> - implements SequentialAgentService { - - private SequentialAgentServiceImpl(Class agentServiceClass) { - super(agentServiceClass); - } - - public static SequentialAgentServiceImpl builder(Class agentServiceClass) { - return new SequentialAgentServiceImpl<>(agentServiceClass); - } - - @Override - public SequentialAgentService subAgents(Object... agents) { - this.workflowBuilder.tasks(t -> t.sequence(agents)); - return this; - } - - @Override - public SequentialAgentService subAgents(List agentExecutors) { - this.subAgents(agentExecutors.toArray()); - return this; - } - - @Override - public SequentialAgentService beforeAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } - - @Override - public SequentialAgentService afterAgentInvocation(Consumer consumer) { - throw new UnsupportedOperationException( - "Feature not implemented yet. See: https://github.com/serverlessworkflow/sdk-java/issues/836"); - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java deleted file mode 100644 index cf3315a96..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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.langchain4j; - -import io.serverlessworkflow.api.types.Workflow; - -public interface WorkflowDefinitionBuilder { - - String META_KEY_OUTPUTNAME = "outputName"; - - Workflow getDefinition(); -} diff --git a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java b/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java deleted file mode 100644 index c3ba1f429..000000000 --- a/experimental/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.UntypedAgent; -import dev.langchain4j.agentic.internal.AgentInvoker; -import dev.langchain4j.agentic.internal.AgentSpecification; -import dev.langchain4j.agentic.internal.AgenticScopeOwner; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.scope.AgenticScopeAccess; -import dev.langchain4j.agentic.scope.AgenticScopeRegistry; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; -import dev.langchain4j.agentic.scope.ResultWithAgenticScope; -import dev.langchain4j.service.MemoryId; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.impl.WorkflowApplication; -import io.serverlessworkflow.impl.model.agentic.langchain4j.AgenticScopeRegistryAssessor; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -public class WorkflowInvocationHandler implements InvocationHandler, AgenticScopeOwner { - - private final Workflow workflow; - private final WorkflowApplication.Builder workflowApplicationBuilder; - private final AgenticScopeRegistryAssessor agenticScopeRegistryAssessor; - - WorkflowInvocationHandler( - Workflow workflow, - WorkflowApplication.Builder workflowApplicationBuilder, - Class agentServiceClass) { - this.workflow = workflow; - this.workflowApplicationBuilder = workflowApplicationBuilder; - this.agenticScopeRegistryAssessor = - new AgenticScopeRegistryAssessor(agentServiceClass.getName()); - } - - @SuppressWarnings("unchecked") - private static void writeAgenticScopeState( - AgenticScope agenticScope, Method method, Object[] args) { - if (method.getDeclaringClass() == UntypedAgent.class) { - agenticScope.writeStates((Map) args[0]); - } else { - Parameter[] parameters = method.getParameters(); - for (int i = 0; i < parameters.length; i++) { - int index = i; - AgentInvoker.optionalParameterName(parameters[i]) - .ifPresent(argName -> agenticScope.writeState(argName, args[index])); - } - } - } - - private String outputKey() { - Object outputName = - this.workflow - .getDocument() - .getMetadata() - .getAdditionalProperties() - .get(WorkflowDefinitionBuilder.META_KEY_OUTPUTNAME); - if (outputName != null) { - return outputName.toString(); - } - return null; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - AgenticScopeRegistry registry = registry(); - // outputKey - if (method.getDeclaringClass() == AgentSpecification.class) { - return switch (method.getName()) { - case "name" -> this.workflow.getDocument().getName(); - case "description" -> this.workflow.getDocument().getSummary(); - case "outputKey" -> outputKey(); - default -> - throw new UnsupportedOperationException( - "Unknown method on AgentInstance class : " + method.getName()); - }; - } - // withAgenticScope - if (method.getDeclaringClass() == AgenticScopeOwner.class) { - // Ingest the workflow input as a AgenticScope object - // Later, retrieve it and start the workflow with it as input. - return switch (method.getName()) { - case "withAgenticScope" -> this.withAgenticScope((DefaultAgenticScope) args[0]); - case "registry" -> registry; - default -> - throw new UnsupportedOperationException( - "Unknown method on AgenticScopeOwner class : " + method.getName()); - }; - } - // getAgenticScope - // evictAgenticScope - if (method.getDeclaringClass() == AgenticScopeAccess.class) { - return switch (method.getName()) { - case "getAgenticScope" -> registry().get(args[0]); - case "evictAgenticScope" -> registry().evict(args[0]); - default -> - throw new UnsupportedOperationException( - "Unknown method on CognisphereAccess class : " + method.getName()); - }; - } - - // invoke - return executeWorkflow(currentAgenticScope(method, args), method, args); - } - - private Object executeWorkflow(AgenticScope agenticScope, Method method, Object[] args) { - writeAgenticScopeState(agenticScope, method, args); - - try (WorkflowApplication app = workflowApplicationBuilder.build()) { - // TODO improve result handling - AgenticScope output = - app.workflowDefinition(workflow) - .instance(agenticScope) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow( - () -> - new IllegalArgumentException( - "Workflow hasn't returned a AgenticScope object.")); - Object result = output.readState(outputKey()); - - return method.getReturnType().equals(ResultWithAgenticScope.class) - ? new ResultWithAgenticScope<>(output, result) - : result; - - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException( - "Failed to execute workflow: " - + workflow.getDocument().getName() - + " - AgenticScope: " - + agenticScope, - e); - } - } - - private AgenticScope currentAgenticScope(Method method, Object[] args) { - Object memoryId = memoryId(method, args); - this.agenticScopeRegistryAssessor.setMemoryId(memoryId); - return this.agenticScopeRegistryAssessor.getAgenticScope(); - } - - private Object memoryId(Method method, Object[] args) { - Parameter[] parameters = method.getParameters(); - for (int i = 0; i < parameters.length; i++) { - if (parameters[i].getAnnotation(MemoryId.class) != null) { - return args[i]; - } - } - return null; - } - - @Override - public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { - this.agenticScopeRegistryAssessor.withAgenticScope(agenticScope); - return this; - } - - @Override - public AgenticScopeRegistry registry() { - return this.agenticScopeRegistryAssessor.registry(); - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java deleted file mode 100644 index 791792c3c..000000000 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.agentic.Agent; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; - -public class Agents { - - public interface CreativeWriter { - - @UserMessage( - """ - You are a creative writer. - Generate a draft of a story long no more than 3 sentence around the given topic. - Return only the story and nothing else. - The topic is {{topic}}. - """) - @Agent("Generate a story based on the given topic") - String generateStory(@V("topic") String topic); - } - - public interface AudienceEditor { - - @UserMessage( - """ - You are a professional editor. - Analyze and rewrite the following story to better align with the target audience of {{audience}}. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edit a story to better fit a given audience") - String editStory(@V("story") String story, @V("audience") String audience); - } - - public interface StyleEditor { - - @UserMessage( - """ - You are a professional editor. - Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edit a story to better fit a given style") - String editStory(@V("story") String story, @V("style") String style); - } -} diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java deleted file mode 100644 index c3be62505..000000000 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.langchain4j; - -import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.ollama.OllamaChatModel; -import java.time.Duration; - -public class Models { - - private static final String OLLAMA_DEFAULT_URL = "http://127.0.0.1:11434"; - public static final String OLLAMA_ENV_URL = System.getenv("OLLAMA_BASE_URL"); - - private static final String OLLAMA_BASE_URL = - OLLAMA_ENV_URL != null ? OLLAMA_ENV_URL : OLLAMA_DEFAULT_URL; - - public static final ChatModel BASE_MODEL = - OllamaChatModel.builder() - .baseUrl(OLLAMA_BASE_URL) - .modelName("qwen2.5:7b") - .timeout(Duration.ofMinutes(10)) - .temperature(0.0) - .logRequests(true) - .logResponses(true) - .build(); - - public static final ChatModel PLANNER_MODEL = - OllamaChatModel.builder() - .baseUrl(OLLAMA_BASE_URL) - .modelName("qwen3:8b") - .timeout(Duration.ofMinutes(10)) - .temperature(0.0) - .logRequests(true) - .logResponses(true) - .build(); -} diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java deleted file mode 100644 index 02ea8e66e..000000000 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.langchain4j; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.CallJava; -import io.serverlessworkflow.api.types.func.OutputAsFunction; -import io.serverlessworkflow.fluent.agentic.AgentsUtils; -import java.util.function.Function; -import org.junit.jupiter.api.Test; - -class SequentialAgentServiceImplTest { - @Test - void shouldBuildEmptyWorkflow_byDefault() { - // given - SequentialAgentServiceImpl service = - SequentialAgentServiceImpl.builder(DummyAgent.class); - - // when - Workflow wf = service.getDefinition(); - - // then - assertNotNull(wf, "Workflow definition should not be null"); - assertTrue(wf.getDo().isEmpty(), "There should be no tasks by default"); - } - - @Test - void shouldApplyBeforeCallConsumer_toInput() { - // given - SequentialAgentServiceImpl service = - (SequentialAgentServiceImpl) - SequentialAgentServiceImpl.builder(DummyAgent.class) - .beforeCall(ctx -> ctx.writeState("foo", "bar")); - - // when - Workflow wf = service.getDefinition(); - - // then - assertNotNull(wf.getInput(), "Input should not be null"); - } - - @Test - void shouldSetOutputName_inDocumentMetadata() { - // given - String outputName = "myOutputName"; - SequentialAgentServiceImpl service = - (SequentialAgentServiceImpl) - SequentialAgentServiceImpl.builder(DummyAgent.class).outputKey(outputName); - - // when - Workflow wf = service.getDefinition(); - - // then - assertNotNull(wf.getDocument().getName(), "Workflow name should not be null"); - assertNotNull(wf.getOutput().getAs(), "Workflow outputAs should not be null"); - assertInstanceOf(OutputAsFunction.class, wf.getOutput().getAs()); - } - - @Test - void shouldSetOutputFunction_extension() { - // given - Function fn = ctx -> 42; - SequentialAgentServiceImpl service = - (SequentialAgentServiceImpl) - SequentialAgentServiceImpl.builder(DummyAgent.class).output(fn); - - // when - Workflow wf = service.getDefinition(); - - // then - assertNotNull(wf.getOutput(), "Output should not be null"); - } - - @Test - void shouldBuildSequenceTasks_withSubAgents() { - // given - Object agentA = AgentsUtils.newMovieExpert(); - Object agentB = AgentsUtils.newMovieExpert(); - SequentialAgentServiceImpl service = - (SequentialAgentServiceImpl) - SequentialAgentServiceImpl.builder(DummyAgent.class).subAgents(agentA, agentB); - - // when - Workflow wf = service.getDefinition(); - - // then - assertEquals(2, wf.getDo().size(), "There should be exactly two tasks"); - - wf.getDo() - .forEach( - t -> { - assertNotNull(t, "Task should not be null"); - Object task = t.getTask().getCallTask().get(); - assertInstanceOf( - CallJava.CallJavaFunction.class, task, "Task should be a CallTaskJava"); - }); - } - - static class DummyAgent {} -} diff --git a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java b/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java deleted file mode 100644 index 32be899b4..000000000 --- a/experimental/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.langchain4j; - -import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.AudienceEditor; -import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.CreativeWriter; -import static io.serverlessworkflow.fluent.agentic.langchain4j.Agents.StyleEditor; -import static io.serverlessworkflow.fluent.agentic.langchain4j.Models.BASE_MODEL; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.UntypedAgent; -import dev.langchain4j.agentic.workflow.WorkflowAgentsBuilder; -import java.util.Map; -import org.junit.jupiter.api.Test; - -public class WorkflowAgentsIT { - - @Test - void sequential_agents_tests() { - WorkflowAgentsBuilder builder = new LC4JWorkflowBuilder(); - - CreativeWriter creativeWriter = - spy( - AgenticServices.agentBuilder(CreativeWriter.class) - .chatModel(BASE_MODEL) - .outputKey("story") - .build()); - - AudienceEditor audienceEditor = - spy( - AgenticServices.agentBuilder(AudienceEditor.class) - .chatModel(BASE_MODEL) - .outputKey("story") - .build()); - - StyleEditor styleEditor = - spy( - AgenticServices.agentBuilder(StyleEditor.class) - .chatModel(BASE_MODEL) - .outputKey("story") - .build()); - - UntypedAgent novelCreator = - builder - .sequenceBuilder() - .subAgents(creativeWriter, audienceEditor, styleEditor) - .outputKey("story") - .build(); - - Map input = - Map.of( - "topic", "dragons and wizards", - "style", "fantasy", - "audience", "young adults"); - - String story = (String) novelCreator.invoke(input); - System.out.println(story); - - verify(creativeWriter).generateStory("dragons and wizards"); - verify(audienceEditor).editStory(any(), eq("young adults")); - verify(styleEditor).editStory(any(), eq("fantasy")); - } -} diff --git a/experimental/fluent/agentic/README.md b/experimental/fluent/agentic/README.md deleted file mode 100644 index 312f9f110..000000000 --- a/experimental/fluent/agentic/README.md +++ /dev/null @@ -1,179 +0,0 @@ -# CNCF Serverless Workflow SDK Java — Agentic DSL - -## What is the DSL? - -This module uses the **CNCF Workflow Specification Java Fluent DSL (DSL 1.0.0, tasks‑based)**. It’s a **strongly‑typed, builder‑style API** for composing workflows in Java while staying faithful to the CNCF spec’s execution model and event semantics. - -**Core ideas:** - -* **Tasks first.** Compose first‑class **tasks** (sequence/branches) rather than legacy “states”. -* **Fluent builders.** Typed verbs guide valid configurations: - - * `agent(id, Agents.Foo)` — **integrates with LangChain4j (LC4J) agentic modules**. Build the agent via `AgenticServices.agentBuilder(...)`, select the model/provider through config, and capture outputs via `outputName`. - * `callFn(id, c -> c.function(MethodRef, ArgClass))` — call **pure Java functions** with static typing. - * `switchCase(id, s -> …)` — branch with **typed predicates** and `onDefault(...)`. - * `emit(id, e -> e.event(...))` — publish **CloudEvents** with typed payload marshalling. - * `listen(id, l -> l.to(e -> e.any(...) / e.all(...)))` — **wait** for one or more events before continuing. -* **Event‑native.** **CloudEvents** are the wire model for `emit`/`listen`, keeping components loosely coupled. -* **Agentic‑AI friendly.** Agents are first‑class tasks; outputs flow into functions and policies naturally. -* **Embeddable runtime.** `WorkflowApplication` runs definitions **in‑process** (great for tests/services). -* **Type‑safe data flow.** Inputs/outputs keep their static types (e.g., `PolicyDecision.class`). - -> [!NOTE] -> **Module layering & mix‑and‑match** -> -> ``` -> spec → func → agentic -> ``` -> -> * **`spec`** — CNCF‑only core (workflows, tasks, events) -> * **`func`** — adds **Java function calls** & **predicate branching** on top of `spec` -> * **`agentic`** — adds **LangChain4j agent calls** on top of `func` + `spec` -> -> Because of this hierarchy, you can **freely mix** `agent(...)` with core CNCF tasks and Java `callFn(...)`/`switchCase(...)` in the same workflow. - ---- - -## Email Drafter Agentic Workflow (use case) - -> [!NOTE] -> The full integration test can be seen in [src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java](). - -**What it does:** -Drafts an email with an agent, **parses** it, runs a **policy check**, and either: - -* **Auto‑marks the email as ready**, or -* **Requests human review** and **waits** for an approval/denial event. - -**Main steps:** - -1. **`agentEmailDrafter`** – `Agents.EmailDrafter` → `email_draft` -2. **`parseDraft`** – `EmailDrafts::parse(String)` → `EmailDraft` -3. **`policyCheck`** – `EmailPolicies::policyCheck(EmailDraft)` → `PolicyDecision` -4. **`needsHumanReview?`** – if decision ≠ `AUTO_SEND`: emit **`org.acme.email.review.required`** and **listen** for `org.acme.email.approved` **or** `org.acme.email.denied` -5. **`emailFinished`** – emit **`org.acme.email.finished`** - -**Mermaid view (generated with our Mermaid library):** - -```mermaid ---- -config: - look: handDrawn - theme: base ---- -flowchart TD - n_agentemaildrafter_760e461ad032@{ shape: rect, label: "call: agentEmailDrafter" } - n_agentemaildrafter_760e461ad032-->n_parsedraft_924cfc80438b - n_parsedraft_924cfc80438b@{ shape: rect, label: "call: parseDraft" } - n_parsedraft_924cfc80438b-->n_policycheck_19f6595cf361 - n_policycheck_19f6595cf361@{ shape: rect, label: "call: policyCheck" } - n_policycheck_19f6595cf361-->n_needshumanreview_b6d9ececf6f3 - n_needshumanreview_b6d9ececf6f3@{ shape: diam, label: "switch: needsHumanReview?" } - n_needshumanreview_b6d9ececf6f3--default-->n_emailready_931221ddff95 - n_needshumanreview_b6d9ececf6f3--default-->n_requestreview_83b9e0aa6873 - n_requestreview_83b9e0aa6873@{ shape: lean-r, label: "emit: **org.acme.email.review.required**" } - n_requestreview_83b9e0aa6873-->n_waitforreview_0e05540cda49 - subgraph n_waitforreview_0e05540cda49["listen"] - direction TB - n_-vntcpr_0@{ shape: note, label: "to ANY events:
• org.acme.email.approved
• org.acme.email.denied" } - n_-vntcpr_0-->n_-qyvbjm_0 - n_-qyvbjm_0@{ shape: f-circ, label: "join" } - n_-qyvbjm_0-->n_-xenlzr_0 - n_-xenlzr_0@{ shape: rect, label: "waitForReview" } - end - n_waitforreview_0e05540cda49-->n_emailready_931221ddff95 - n_emailready_931221ddff95@{ shape: lean-r, label: "emit: **org.acme.email.finished**" } - n_emailready_931221ddff95-->n__end__ - n__start__@{ shape: sm-circ, label: "Start" } - n__start__-->n_agentemaildrafter_760e461ad032 - n__end__@{ shape: stop, label: "End" } -``` - ---- - -## Human‑in‑the‑Loop (HITL) in Java Enterprise: Why & How - -**Why HITL matters:** In real organizations, certain actions must be **reviewed or approved by a person** before they’re executed. Reasons include compliance (SOX, GDPR/PII), brand/reputation risk, contractual obligations, or simply to protect end‑users. With this DSL you can **codify those gates** while keeping the rest fully automated. - -### What the Email Drafter workflow demonstrates - -* **Risk‑based gating.** A policy converts the draft into a `PolicyDecision`. If risk is low → `AUTO_SEND`; else we **emit** `org.acme.email.review.required` and **listen** for approval/denial. -* **Asynchronous review.** Business users can take minutes or hours. The workflow stays **durably waiting** via `listen(...any(...))` and resumes when an event arrives. -* **Auditable trail.** Every `emit`/`listen` edge is a CloudEvent, so you get a clear audit trail: *who* approved, *when*, and *what* changed. - -### Where this shines in Java enterprise apps - -* **Customer communications & CRM.** Outbound emails, quotes, and renewals requiring manager/legal sign‑off. -* **Procurement & finance.** PO creation, vendor onboarding, invoice disputes, refunds above threshold. -* **HR & legal.** Offer letters, policy updates, external statements that need counsel approval. -* **ITSM & DevOps.** Change approvals (CAB), production runbooks that pause for human confirmation. -* **Healthcare & insurance.** Sensitive messaging that must be clinician/adjuster‑approved. -* **Marketing & brand.** Campaign copy generation with brand/compliance review before release. - -### A typical HITL architecture (event‑native) - -1. **Workflow emits** `org.acme.email.review.required` with a correlation key (e.g., `data.workflowInstanceId`). -2. **A reviewer UI** (React/Angular) lists pending items by reading from Kafka/AMQP or via a service ( - Quarkus/Spring) that projects CloudEvents into a DB. -3. Reviewer **approves/denies** → UI calls a webhook - (`POST /events/decisions`) that **publishes** a CloudEvent: - - * `org.acme.email.approved` **or** `org.acme.email.denied` - * includes the same correlation key so the waiting `listen` matches -4. Workflow **resumes** and continues to `emailReady` or an alternate path. - -> [!TIP] -> Use CloudEvents attributes like `subject` (correlation), `type` (routing), and `time` (auditing). Store the event IDs to ensure **idempotency** if a reviewer double‑clicks. - -### Example approval payload (conceptual) - -```json -{ - "type": "org.acme.email.approved", - "subject": "wf:emailDrafterAgentic:2b9ee...", - "data": { - "approvedBy": "jdoe", - "comments": "Looks good; added calendar link.", - "redactions": ["phoneNumber"], - "version": 3 - } -} -``` - -### Production considerations & best practices - -* **Timeouts & escalation.** Add a timer branch (SLA breach → notify Slack/Jira or fall back to a safe default). -* **Policy engines.** Externalize complex rules with **OPA** or a Java rules engine; keep `policyCheck` deterministic and testable. -* **PII/redaction gates.** Run a redaction/safety step before approvals (PII scans, external domain checks, prompt‑injection guards). -* **RBAC & separation of duties.** Ensure the reviewer isn’t the same person who drafted (auditors love this). -* **Observability.** Emit metrics on time‑to‑approve, auto‑send rate, denial reasons; add tracing across `emit`/`listen`. -* **Idempotency & retries.** Use event IDs and outbox/inbox patterns with Kafka/AMQP to avoid duplicate advances. -* **Versioning.** Include draft `version` in events; if the text changed during review, request re‑approval. - -### Extending the pattern - -* **Multi‑stage approvals.** Chain multiple `listen` steps (e.g., manager → legal → compliance) with `any/all` strategies. -* **Conditional reviewers.** Route to different queues based on region/domain or risk score. -* **Partial automation.** Allow **auto‑send** for internal domains but require approval for external ones (`allowedDomains`). -* **A/B safety.** Run two agents (draft + safety critique) before human review; only request HITL if they disagree. - -This approach gives teams the **best of both worlds**: fast, LLM‑assisted generation with **governed, observable checkpoints** that fit naturally into Java enterprise stacks (Quarkus/Spring, JPA, Kafka/AMQP, REST/WebSockets) and compliance programs. - ---- - -## Maven setup (single dependency) - -**For application projects** a **single dependency** is enough. The `agentic` module brings the required transitive bits for you (core spec, func layer, runtime, etc.). - -Requires **Java 17+**. - -```xml - - io.serverlessworkflow - serverlessworkflow-experimental-fluent-agentic - YOUR_VERSION - -``` - -> [!TIP] -> You can still mix `agent(...)` calls with `callFn(...)`, `switchCase(...)`, `emit(...)`, and `listen(...)` in the same workflow thanks to the `spec → func → agentic` layering. diff --git a/experimental/fluent/agentic/pom.xml b/experimental/fluent/agentic/pom.xml deleted file mode 100644 index 6329f05d1..000000000 --- a/experimental/fluent/agentic/pom.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - 4.0.0 - - io.serverlessworkflow - serverlessworkflow-experimental-fluent - 8.0.0-SNAPSHOT - - - Serverless Workflow :: Experimental :: Fluent :: Agentic - serverlessworkflow-experimental-fluent-agentic - - - - io.serverlessworkflow - serverlessworkflow-experimental-types - - - io.serverlessworkflow - serverlessworkflow-experimental-fluent-func - - - dev.langchain4j - langchain4j-agentic - - - - org.slf4j - slf4j-simple - test - - - org.junit.jupiter - junit-jupiter-api - test - - - org.mockito - mockito-core - test - - - org.assertj - assertj-core - test - - - dev.langchain4j - langchain4j-ollama - test - ${version.dev.langchain4j} - - - io.serverlessworkflow - serverlessworkflow-experimental-agentic - test - - - io.cloudevents - cloudevents-json-jackson - test - - - io.serverlessworkflow - serverlessworkflow-impl-jackson - ${project.version} - test - - - - - - - maven-jar-plugin - ${version.jar.plugin} - - - - - test-jar - - - - - - - \ No newline at end of file diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java deleted file mode 100644 index d8a2d0159..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static dev.langchain4j.agentic.internal.AgentUtil.agentsToExecutors; - -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; -import io.serverlessworkflow.api.types.func.LoopPredicateIndex; -import java.util.List; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.Predicate; - -public final class AgentAdapters { - - private AgentAdapters() {} - - public static List toExecutors(Object... agents) { - return agentsToExecutors(agents); - } - - public static Function toFunction(AgentExecutor exec) { - return exec::execute; - } - - public static LoopPredicateIndex toWhile(Predicate exit) { - return (model, item, idx) -> !exit.test(model); - } - - public static LoopPredicateIndex toWhile( - BiPredicate exit) { - return (model, item, idx) -> !exit.test(model, idx); - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java deleted file mode 100644 index 4d7c42a22..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import io.serverlessworkflow.fluent.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.FuncSetTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; -import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.spec.BaseDoTaskBuilder; -import java.util.function.Consumer; - -public class AgentDoTaskBuilder - extends BaseDoTaskBuilder - implements ConditionalTaskBuilder, AgentDoFluent { - - public AgentDoTaskBuilder() { - super(new AgentTaskItemListBuilder()); - } - - @Override - protected AgentDoTaskBuilder self() { - return this; - } - - @Override - public AgentDoTaskBuilder agent(String name, Object agent) { - this.listBuilder().agent(name, agent); - return self(); - } - - @Override - public AgentDoTaskBuilder sequence(String name, Object... agents) { - this.listBuilder().sequence(name, agents); - return self(); - } - - @Override - public AgentDoTaskBuilder loop(String name, Consumer builder) { - this.listBuilder().loop(name, builder); - return self(); - } - - @Override - public AgentDoTaskBuilder loop(String name, LoopAgentsBuilder builder) { - this.listBuilder().loop(name, builder); - return self(); - } - - @Override - public AgentDoTaskBuilder parallel(String name, Object... agents) { - this.listBuilder().parallel(name, agents); - return self(); - } - - @Override - public AgentDoTaskBuilder function(String name, Consumer cfg) { - this.listBuilder().function(name, cfg); - return self(); - } - - @Override - public AgentDoTaskBuilder emit(String name, Consumer itemsConfigurer) { - this.listBuilder().emit(name, itemsConfigurer); - 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); - return self(); - } - - @Override - public AgentDoTaskBuilder fork(String name, Consumer itemsConfigurer) { - this.listBuilder().fork(name, itemsConfigurer); - return self(); - } - - @Override - public AgentDoTaskBuilder set(String name, Consumer itemsConfigurer) { - this.listBuilder().set(name, itemsConfigurer); - return self(); - } - - @Override - public AgentDoTaskBuilder set(String name, String expr) { - this.listBuilder().set(name, expr); - return self(); - } - - @Override - public AgentDoTaskBuilder switchCase( - String name, Consumer itemsConfigurer) { - this.listBuilder().switchCase(name, itemsConfigurer); - return self(); - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java deleted file mode 100644 index 141e711c1..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentListenTaskBuilder.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import io.serverlessworkflow.api.types.AnyEventConsumptionStrategy; -import io.serverlessworkflow.api.types.ListenTask; -import io.serverlessworkflow.api.types.func.UntilPredicate; -import io.serverlessworkflow.fluent.func.FuncListenToBuilder; -import io.serverlessworkflow.fluent.func.spi.ConditionalTaskBuilder; -import io.serverlessworkflow.fluent.spec.AbstractListenTaskBuilder; -import java.util.function.Predicate; - -public class AgentListenTaskBuilder - extends AbstractListenTaskBuilder - implements ConditionalTaskBuilder { - - private UntilPredicate untilPredicate; - - public AgentListenTaskBuilder() { - super(new AgentTaskItemListBuilder()); - } - - @Override - protected AgentListenTaskBuilder self() { - return this; - } - - @Override - protected FuncListenToBuilder newEventConsumptionStrategyBuilder() { - return new FuncListenToBuilder(); - } - - 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/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java deleted file mode 100644 index d48203120..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.scope.DefaultAgenticScope; -import io.serverlessworkflow.api.types.Task; -import io.serverlessworkflow.api.types.TaskItem; -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.FuncSetTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; -import io.serverlessworkflow.fluent.spec.BaseTaskItemListBuilder; -import java.util.List; -import java.util.function.Consumer; - -public class AgentTaskItemListBuilder extends BaseTaskItemListBuilder - implements AgentDoFluent { - - private final FuncTaskItemListBuilder delegate; - - public AgentTaskItemListBuilder() { - super(); - this.delegate = new FuncTaskItemListBuilder(super.mutableList()); - } - - @Override - protected AgentTaskItemListBuilder self() { - return this; - } - - @Override - protected AgentTaskItemListBuilder newItemListBuilder() { - return new AgentTaskItemListBuilder(); - } - - @Override - public AgentTaskItemListBuilder agent(String name, Object agent) { - AgentAdapters.toExecutors(agent) - .forEach( - exec -> - this.delegate.function( - name, - fn -> fn.function(AgentAdapters.toFunction(exec), DefaultAgenticScope.class))); - return self(); - } - - @Override - public AgentTaskItemListBuilder sequence(String name, Object... agents) { - for (int i = 0; i < agents.length; i++) { - agent(name + "-" + i, agents[i]); - } - return self(); - } - - @Override - public AgentTaskItemListBuilder loop(String name, Consumer consumer) { - final LoopAgentsBuilder builder = new LoopAgentsBuilder(); - consumer.accept(builder); - this.loop(name, builder); - return self(); - } - - @Override - public AgentTaskItemListBuilder loop(String name, LoopAgentsBuilder builder) { - this.addTaskItem(new TaskItem(name, new Task().withForTask(builder.build()))); - return self(); - } - - @Override - public AgentTaskItemListBuilder parallel(String name, Object... agents) { - this.delegate.fork( - name, - fork -> { - List execs = AgentAdapters.toExecutors(agents); - for (int i = 0; i < execs.size(); i++) { - AgentExecutor ex = execs.get(i); - String agentName = - ex.agentInvoker().name() != null - ? ex.agentInvoker().name() - : "branch-" + i + "-" + name; - fork.branch(agentName, AgentAdapters.toFunction(ex), DefaultAgenticScope.class); - } - }); - return self(); - } - - @Override - public AgentTaskItemListBuilder function(String name, Consumer cfg) { - this.delegate.function(name, cfg); - return self(); - } - - @Override - public AgentTaskItemListBuilder emit(String name, Consumer itemsConfigurer) { - this.delegate.emit(name, itemsConfigurer); - return self(); - } - - @Override - public AgentTaskItemListBuilder listen( - String name, Consumer itemsConfigurer) { - final AgentListenTaskBuilder builder = new AgentListenTaskBuilder(); - itemsConfigurer.accept(builder); - this.addTaskItem(new TaskItem(name, new Task().withListenTask(builder.build()))); - return self(); - } - - @Override - public AgentTaskItemListBuilder forEach( - String name, Consumer itemsConfigurer) { - this.delegate.forEach(name, itemsConfigurer); - return self(); - } - - @Override - public AgentTaskItemListBuilder fork(String name, Consumer itemsConfigurer) { - this.delegate.fork(name, itemsConfigurer); - return self(); - } - - @Override - public AgentTaskItemListBuilder set(String name, Consumer itemsConfigurer) { - this.delegate.set(name, itemsConfigurer); - return self(); - } - - @Override - public AgentTaskItemListBuilder set(String name, String expr) { - this.delegate.set(name, expr); - return self(); - } - - @Override - public AgentTaskItemListBuilder switchCase( - String name, Consumer itemsConfigurer) { - this.delegate.switchCase(name, itemsConfigurer); - return self(); - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java deleted file mode 100644 index 5472904f6..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.fluent.func.spi.FuncTransformations; -import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.function.Predicate; - -public class AgentWorkflowBuilder - extends BaseWorkflowBuilder - implements FuncTransformations { - - private final List agentDoTaskBuilders; - - AgentWorkflowBuilder(final String name, final String namespace, final String version) { - super(name, namespace, version); - agentDoTaskBuilders = new ArrayList<>(); - } - - public static AgentWorkflowBuilder workflow() { - return new AgentWorkflowBuilder( - UUID.randomUUID().toString(), DEFAULT_NAMESPACE, DEFAULT_VERSION); - } - - public static AgentWorkflowBuilder workflow(String name) { - return new AgentWorkflowBuilder(name, DEFAULT_NAMESPACE, DEFAULT_VERSION); - } - - public static AgentWorkflowBuilder workflow(String name, String ns) { - return new AgentWorkflowBuilder(name, ns, DEFAULT_VERSION); - } - - public AgentWorkflowBuilder agent(Object agent) { - return agent(UUID.randomUUID().toString(), agent); - } - - public AgentWorkflowBuilder agent(String name, Object agent) { - final AgentDoTaskBuilder doTaskBuilder = this.newDo(); - doTaskBuilder.agent(name, agent); - agentDoTaskBuilders.add(doTaskBuilder); - return this; - } - - public AgentWorkflowBuilder sequence(Object... agents) { - return sequence(UUID.randomUUID().toString(), agents); - } - - public AgentWorkflowBuilder sequence(String name, Object... agents) { - final AgentDoTaskBuilder doTaskBuilder = this.newDo(); - doTaskBuilder.sequence(name, agents); - agentDoTaskBuilders.add(doTaskBuilder); - return this; - } - - public AgentWorkflowBuilder parallel(Object... agents) { - return this.parallel(UUID.randomUUID().toString(), agents); - } - - public AgentWorkflowBuilder parallel(String name, Object... agents) { - final AgentDoTaskBuilder doTaskBuilder = this.newDo(); - doTaskBuilder.parallel(name, agents); - agentDoTaskBuilders.add(doTaskBuilder); - return this; - } - - public AgentWorkflowBuilder loop(Predicate exitCondition, Object... agents) { - return this.loop(UUID.randomUUID().toString(), exitCondition, agents); - } - - public AgentWorkflowBuilder loop( - String name, Predicate exitCondition, Object... agents) { - final AgentDoTaskBuilder doTaskBuilder = this.newDo(); - doTaskBuilder.loop(name, loop -> loop.subAgents(agents).exitCondition(exitCondition)); - agentDoTaskBuilders.add(doTaskBuilder); - return this; - } - - @Override - protected AgentDoTaskBuilder newDo() { - return new AgentDoTaskBuilder(); - } - - @Override - protected AgentWorkflowBuilder self() { - return this; - } - - @Override - public Workflow build() { - List items = new ArrayList<>(workflow.getDo()); - this.agentDoTaskBuilders.stream() - .map(AgentDoTaskBuilder::build) - .forEach(b -> items.addAll(b.getDo())); - this.workflow.setDo(Collections.unmodifiableList(items)); - this.agentDoTaskBuilders.clear(); - return this.workflow; - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflow.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflow.java deleted file mode 100644 index ae5d483ca..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflow.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.agentic.Agent; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.service.V; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.impl.WorkflowApplication; -import io.serverlessworkflow.impl.WorkflowModel; -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -public class AgenticWorkflow { - - private final Class agent; - - private AgentWorkflowBuilder builder; - - private AgenticWorkflow(Class agent) { - this.agent = agent; - } - - public static AgenticWorkflow of(Class agent) { - return new AgenticWorkflow<>(agent); - } - - public AgenticWorkflow flow(AgentWorkflowBuilder builder) { - this.builder = builder; - return this; - } - - public T build() { - Objects.requireNonNull( - builder, "AgenticServices.flow(AgentWorkflowBuilder) must be called before build()"); - Workflow workflow = builder.build(); - return AgenticServiceBuilder.create(agent, new AgentInvocationHandler(workflow)); - } - - private static class AgenticServiceBuilder { - - @SuppressWarnings("unchecked") - public static T create(Class runner, InvocationHandler h) { - if (!runner.isInterface()) { - throw new IllegalArgumentException(runner + " must be an interface to create a Proxy"); - } - - ClassLoader cl = runner.getClassLoader(); - Class[] ifaces = new Class[] {runner}; - return (T) Proxy.newProxyInstance(cl, ifaces, h); - } - } - - private class AgentInvocationHandler implements InvocationHandler { - - private final Workflow workflow; - - public AgentInvocationHandler(Workflow workflow) { - this.workflow = workflow; - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) { - if (method.getDeclaringClass() == Object.class) { - return switch (method.getName()) { - case "toString" -> "AgentProxy(" + workflow.getDocument().getName() + ")"; - case "hashCode" -> System.identityHashCode(proxy); - case "equals" -> proxy == args[0]; - default -> throw new IllegalStateException("Unexpected Object method: " + method); - }; - } - - Agent agent = method.getAnnotation(Agent.class); - if (agent == null) { - throw new IllegalStateException( - "Method " + method.getName() + " is not annotated with @Agent"); - } - - Annotation[][] annotations = method.getParameterAnnotations(); - Map input = new HashMap<>(); - for (int i = 0; i < annotations.length; i++) { - boolean found = false; - for (Annotation a : annotations[i]) { - if (a instanceof V) { - String key = ((V) a).value(); - Object value = args[i]; - input.put(key, value); - found = true; - break; - } - } - if (!found) { - throw new IllegalStateException( - "Parameter " - + (i + 1) - + " of method " - + method.getName() - + " is not annotated with @V"); - } - } - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - WorkflowModel result = app.workflowDefinition(workflow).instance(input).start().get(); - if (result.asJavaObject() instanceof AgenticScope scope) { - Object out = scope.state().get("input"); - if (out != null) { - return out; - } - } - return result.asJavaObject(); - } catch (Exception e) { - throw new RuntimeException("Workflow execution failed", e); - } - } - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java deleted file mode 100644 index dd1d6c00d..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.api.types.ForTaskConfiguration; -import io.serverlessworkflow.api.types.func.ForTaskFunction; -import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; -import java.util.List; -import java.util.UUID; -import java.util.function.BiPredicate; -import java.util.function.ObjIntConsumer; -import java.util.function.Predicate; -import java.util.stream.IntStream; - -public class LoopAgentsBuilder { - - private final FuncTaskItemListBuilder funcDelegate; - private final ForTaskFunction forTask; - - private int maxIterations = 1024; - - public LoopAgentsBuilder() { - this.forTask = new ForTaskFunction(); - this.forTask.setFor(new ForTaskConfiguration()); - this.funcDelegate = new FuncTaskItemListBuilder(); - } - - private static void forEachIndexed(List list, ObjIntConsumer consumer) { - IntStream.range(0, list.size()).forEach(i -> consumer.accept(list.get(i), i)); - } - - public LoopAgentsBuilder subAgents(String baseName, Object... agents) { - List execs = AgentAdapters.toExecutors(agents); - forEachIndexed( - execs, - (exec, idx) -> - funcDelegate.function( - baseName + "-" + idx, fn -> fn.function(AgentAdapters.toFunction(exec)))); - return this; - } - - public LoopAgentsBuilder subAgents(Object... agents) { - return this.subAgents("agent-" + UUID.randomUUID(), agents); - } - - public LoopAgentsBuilder maxIterations(int maxIterations) { - this.maxIterations = maxIterations; - return this; - } - - public LoopAgentsBuilder exitCondition(Predicate exitCondition) { - this.forTask.withWhile(AgentAdapters.toWhile(exitCondition), AgenticScope.class); - return this; - } - - public LoopAgentsBuilder exitCondition(BiPredicate exitCondition) { - this.forTask.withWhile(AgentAdapters.toWhile(exitCondition), AgenticScope.class); - return this; - } - - public ForTaskFunction build() { - this.forTask.setDo(this.funcDelegate.build()); - this.forTask.withCollection(ignored -> IntStream.range(0, maxIterations).boxed().toList()); - return this.forTask; - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java deleted file mode 100644 index 78df29218..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentListenConfigurer.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.configurers; - -import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; -import java.util.function.Consumer; - -@FunctionalInterface -public interface AgentListenConfigurer extends Consumer {} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java deleted file mode 100644 index d0d2417ed..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/configurers/AgentTaskConfigurer.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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.configurers; - -import io.serverlessworkflow.fluent.agentic.AgentDoTaskBuilder; -import java.util.function.Consumer; - -@FunctionalInterface -public interface AgentTaskConfigurer extends Consumer {} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java deleted file mode 100644 index 1f75daa3e..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgentListenSpec.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.dsl; - -import io.serverlessworkflow.fluent.agentic.AgentListenTaskBuilder; -import io.serverlessworkflow.fluent.agentic.configurers.AgentListenConfigurer; -import io.serverlessworkflow.fluent.func.dsl.BaseFuncListenSpec; -import io.serverlessworkflow.fluent.spec.AbstractListenTaskBuilder; - -public final class AgentListenSpec - extends BaseFuncListenSpec - implements AgentListenConfigurer { - - public AgentListenSpec() { - super(AbstractListenTaskBuilder::to); - } - - @Override - protected AgentListenSpec self() { - return this; - } - - @Override - public void accept(AgentListenTaskBuilder agentListenTaskBuilder) { - acceptInto(agentListenTaskBuilder); - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java deleted file mode 100644 index fbaa7e00c..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/dsl/AgenticDSL.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.dsl; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.cloudevents.CloudEventData; -import io.serverlessworkflow.api.types.FlowDirectiveEnum; -import io.serverlessworkflow.fluent.agentic.AgentDoTaskBuilder; -import io.serverlessworkflow.fluent.agentic.configurers.AgentTaskConfigurer; -import io.serverlessworkflow.fluent.func.FuncCallTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncEmitTaskBuilder; -import io.serverlessworkflow.fluent.func.FuncSwitchTaskBuilder; -import io.serverlessworkflow.fluent.func.configurers.FuncPredicateEventConfigurer; -import io.serverlessworkflow.fluent.func.configurers.SwitchCaseConfigurer; -import io.serverlessworkflow.fluent.func.dsl.ReflectionUtils; -import io.serverlessworkflow.fluent.func.dsl.SwitchCaseSpec; -import io.serverlessworkflow.fluent.func.dsl.internal.CommonFuncOps; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Predicate; - -public final class AgenticDSL { - - private static final CommonFuncOps OPS = new CommonFuncOps() {}; - - private AgenticDSL() {} - - public static Consumer fn( - Function function, Class argClass) { - return OPS.fn(function, argClass); - } - - public static Consumer fn(Function function) { - return OPS.fn(function); - } - - public static Consumer cases(SwitchCaseConfigurer... cases) { - return OPS.cases(cases); - } - - public static SwitchCaseSpec caseOf(Predicate when, Class whenClass) { - return OPS.caseOf(when, whenClass); - } - - public static SwitchCaseSpec caseOf(Predicate when) { - return OPS.caseOf(when); - } - - public static SwitchCaseConfigurer caseDefault(String task) { - return OPS.caseDefault(task); - } - - public static SwitchCaseConfigurer caseDefault(FlowDirectiveEnum directive) { - return OPS.caseDefault(directive); - } - - public static AgentListenSpec to() { - return new AgentListenSpec(); - } - - public static AgentListenSpec toOne(String type) { - return new AgentListenSpec().one(e -> e.type(type)); - } - - public static AgentListenSpec toAll(String... types) { - FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; - for (int i = 0; i < types.length; i++) { - events[i] = event(types[i]); - } - return new AgentListenSpec().all(events); - } - - public static AgentListenSpec toAny(String... types) { - FuncPredicateEventConfigurer[] events = new FuncPredicateEventConfigurer[types.length]; - for (int i = 0; i < types.length; i++) { - events[i] = event(types[i]); - } - return new AgentListenSpec().any(events); - } - - public static FuncPredicateEventConfigurer event(String type) { - return OPS.event(type); - } - - public static Consumer event( - String type, Function function) { - return OPS.event(type, function); - } - - public static Consumer event( - String type, Function function, Class clazz) { - return OPS.event(type, function, clazz); - } - - // -------- Agentic Workflow Patterns -------- // - public static AgentTaskConfigurer sequence(Object... agents) { - return list -> list.sequence(agents); - } - - public static AgentTaskConfigurer sequence(Predicate predicate, Object... agents) { - return list -> list.sequence(agents).when(predicate); - } - - public static AgentTaskConfigurer loop(Predicate exitCondition, Object... agents) { - return list -> list.loop(l -> l.subAgents(agents).exitCondition(exitCondition)); - } - - public static AgentTaskConfigurer loop( - int maxIterations, Predicate exitCondition, Object... agents) { - return list -> - list.loop( - l -> l.subAgents(agents).exitCondition(exitCondition).maxIterations(maxIterations)); - } - - public static AgentTaskConfigurer parallel(Object... agents) { - return list -> list.parallel(agents); - } - - public static AgentTaskConfigurer parallel(Predicate predicate, Object... agents) { - return list -> list.parallel(agents).when(predicate); - } - - // --------- Tasks ------ // - public static Consumer doTasks(AgentTaskConfigurer... steps) { - Objects.requireNonNull(steps, "Steps in a tasks are required"); - final List snapshot = List.of(steps.clone()); - return list -> snapshot.forEach(s -> s.accept(list)); - } - - public static AgentTaskConfigurer function(Function function, Class argClass) { - return list -> list.function(fn(function, argClass)); - } - - public static AgentTaskConfigurer function(Function function) { - Class clazz = ReflectionUtils.inferInputType(function); - return list -> list.function(fn(function, clazz)); - } - - public static AgentTaskConfigurer agent(Object agent) { - return list -> list.agent(agent); - } - - public static AgentTaskConfigurer conditional(Predicate predicate, Object agent) { - return list -> list.agent(agent).when(predicate); - } - - public static AgentTaskConfigurer emit(Consumer event) { - return list -> list.emit(event); - } - - public static AgentTaskConfigurer switchCase(Consumer switchCase) { - return list -> list.switchCase(switchCase); - } -} diff --git a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java b/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java deleted file mode 100644 index 1b0f05dd8..000000000 --- a/experimental/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.spi; - -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; -import java.util.UUID; -import java.util.function.Consumer; - -public interface AgentDoFluent> - extends SetFluent, - EmitFluent, - ForEachFluent, - SwitchFluent, - ForkFluent, - ListenFluent, - CallFnFluent { - - SELF agent(String name, Object agent); - - default SELF agent(Object agent) { - return agent(UUID.randomUUID().toString(), agent); - } - - SELF sequence(String name, Object... agents); - - default SELF sequence(Object... agents) { - return sequence("seq-" + UUID.randomUUID(), agents); - } - - SELF loop(String name, Consumer builder); - - default SELF loop(Consumer builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } - - SELF loop(String name, LoopAgentsBuilder builder); - - default SELF loop(LoopAgentsBuilder builder) { - return loop("loop-" + UUID.randomUUID(), builder); - } - - SELF parallel(String name, Object... agents); - - default SELF parallel(Object... agents) { - return parallel("par-" + UUID.randomUUID(), agents); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentDslWorkflowTest.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentDslWorkflowTest.java deleted file mode 100644 index 2b6947fd2..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentDslWorkflowTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; -import static org.assertj.core.api.Assertions.assertThat; - -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.CallTaskJava; -import io.serverlessworkflow.api.types.func.ForTaskFunction; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class AgentDslWorkflowTest { - - @Test - @DisplayName("Sequential agents via DSL.sequence(...)") - void dslSequentialAgents() { - var a1 = AgentsUtils.newMovieExpert(); - var a2 = AgentsUtils.newMovieExpert(); - var a3 = AgentsUtils.newMovieExpert(); - - Workflow wf = workflow("seqFlow").tasks(tasks -> tasks.sequence("process", a1, a2, a3)).build(); - - this.assertSequentialAgents(wf); - } - - @Test - @DisplayName("Sequential agents via DSL.sequence(...)") - void dslSequentialAgentsShortcut() { - var a1 = AgentsUtils.newMovieExpert(); - var a2 = AgentsUtils.newMovieExpert(); - var a3 = AgentsUtils.newMovieExpert(); - - Workflow wf = workflow("seqFlow").sequence("process", a1, a2, a3).build(); - - this.assertSequentialAgents(wf); - } - - private void assertSequentialAgents(Workflow wf) { - List items = wf.getDo(); - assertThat(items).hasSize(3); - // names should be process-0, process-1, process-2 - assertThat(items.get(0).getName()).isEqualTo("process-0"); - assertThat(items.get(1).getName()).isEqualTo("process-1"); - assertThat(items.get(2).getName()).isEqualTo("process-2"); - // each is a CallTaskJava under the hood - items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); - } - - @Test - @DisplayName("Bare Java‑bean call via DSL.callFn(...)") - void dslCallFnBare() { - Workflow wf = - workflow("beanCall") - .tasks(tasks -> tasks.function("plainCall", fn -> fn.function(ctx -> "pong"))) - .build(); - - List items = wf.getDo(); - assertThat(items).hasSize(1); - TaskItem ti = items.get(0); - assertThat(ti.getName()).isEqualTo("plainCall"); - assertThat(ti.getTask().getCallTask()).isInstanceOf(CallTaskJava.class); - } - - @Test - void dslLoopAgents() { - var scorer = AgentsUtils.newMovieExpert(); // re‑using MovieExpert as a stand‑in for scoring - var editor = AgentsUtils.newMovieExpert(); // likewise - - Workflow wf = - AgentWorkflowBuilder.workflow("retryFlow") - .loop("reviewLoop", c -> c.readState("score", 0).doubleValue() > 0.75, scorer, editor) - .build(); - - List items = wf.getDo(); - assertThat(items).hasSize(1); - - var fn = (ForTaskFunction) items.get(0).getTask().getForTask(); - assertThat(fn.getDo()).isNotNull(); - assertThat(fn.getDo()).hasSize(2); - fn.getDo() - .forEach(si -> assertThat(si.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); - } - - @Test - void dslParallelAgents() { - var a1 = AgentsUtils.newMovieExpert(); - var a2 = AgentsUtils.newMovieExpert2(); - - Workflow wf = workflow("forkFlow").parallel("fanout", a1, a2).build(); - - List items = wf.getDo(); - assertThat(items).hasSize(1); - - var fork = items.get(0).getTask().getForkTask(); - // two branches created - assertThat(fork.getFork().getBranches()).hasSize(2); - // branch names follow "branch-{index}-{name}" - assertThat(fork.getFork().getBranches().get(0).getName()).isEqualTo("findMovie"); - assertThat(fork.getFork().getBranches().get(1).getName()).isEqualTo("findMovie2"); - } - - @Test - void dslMixSpecAndAgent() { - var agent = AgentsUtils.newMovieExpert(); - - Workflow wf = - workflow("mixedFlow") - .tasks( - tasks -> - tasks - .set("init", s -> s.expr("$.initialized = true")) - .agent("callAgent", agent) - .set("done", "$.status = 'OK'")) - .build(); - - List items = wf.getDo(); - assertThat(items).hasSize(3); - // init is a SetTask - assertThat(items.get(0).getTask().getSetTask()).isNotNull(); - // agent call becomes a CallTaskJava - assertThat(items.get(1).getTask().getCallTask()).isInstanceOf(CallTaskJava.class); - // done is another SetTask - assertThat(items.get(2).getTask().getSetTask()).isNotNull(); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java deleted file mode 100644 index 8b9585db9..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.serverlessworkflow.api.types.Task; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.func.CallTaskJava; -import io.serverlessworkflow.api.types.func.ForTaskFunction; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -/** Structural tests for AgentTaskItemListBuilder. */ -class AgentTaskItemListBuilderTest { - - @Test - @DisplayName("agent(name,obj) adds a CallTaskJava-backed TaskItem") - void testAgentAddsCallTask() { - AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); - Agents.MovieExpert agent = AgentsUtils.newMovieExpert(); - - b.agent("my-agent", agent); - - List items = b.build(); - assertThat(items).hasSize(1); - TaskItem item = items.get(0); - assertThat(item.getName()).isEqualTo("my-agent"); - - Task task = item.getTask(); - assertThat(task.getCallTask()).isInstanceOf(CallTaskJava.class); - } - - @Test - @DisplayName("sequence(name, agents...) expands to N CallTask items, in order") - void testSequence() { - AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); - Agents.MovieExpert a1 = AgentsUtils.newMovieExpert(); - Agents.MovieExpert a2 = AgentsUtils.newMovieExpert(); - Agents.MovieExpert a3 = AgentsUtils.newMovieExpert(); - - b.sequence("seq", a1, a2, a3); - - List items = b.build(); - assertThat(items).hasSize(3); - assertThat(items.get(0).getName()).isEqualTo("seq-0"); - assertThat(items.get(1).getName()).isEqualTo("seq-1"); - assertThat(items.get(2).getName()).isEqualTo("seq-2"); - - // All must be call branche - items.forEach(it -> assertThat(it.getTask().getCallTask().get()).isNotNull()); - } - - @Test - @DisplayName("loop(name, builder) produces a ForTaskFunction with inner call branche") - void testLoop() { - AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); - Agents.MovieExpert scorer = AgentsUtils.newMovieExpert(); - Agents.MovieExpert editor = AgentsUtils.newMovieExpert(); - - b.loop("rev-loop", loop -> loop.subAgents("inner", scorer, editor)); - - List items = b.build(); - assertThat(items).hasSize(1); - - TaskItem loopItem = items.get(0); - ForTaskFunction forFn = (ForTaskFunction) loopItem.getTask().getForTask(); - assertThat(forFn).isNotNull(); - assertThat(forFn.getDo()).hasSize(2); // scorer + editor inside - assertThat(forFn.getDo().get(0).getTask().getCallTask().get()).isNotNull(); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java deleted file mode 100644 index 621007deb..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newMovieExpert; -import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.spy; - -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.api.types.ForkTask; -import io.serverlessworkflow.api.types.Task; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.CallJava; -import io.serverlessworkflow.api.types.func.ForTaskFunction; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Predicate; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -class AgentWorkflowBuilderTest { - - @Test - public void verifyAgentCall() { - Agents.MovieExpert movieExpert = - spy( - AgenticServices.agentBuilder(Agents.MovieExpert.class) - .outputKey("movies") - .chatModel(BASE_MODEL) - .build()); - - Workflow workflow = - AgentWorkflowBuilder.workflow().tasks(tasks -> tasks.agent("myAgent", movieExpert)).build(); - - assertNotNull(workflow); - assertEquals(1, workflow.getDo().size()); - assertInstanceOf(CallJava.class, workflow.getDo().get(0).getTask().getCallTask().get()); - } - - @Test - void sequenceAgents() { - Agents.MovieExpert movieExpert = newMovieExpert(); - Workflow wf = - AgentWorkflowBuilder.workflow("seqFlow") - .tasks(d -> d.sequence("lineup", movieExpert, movieExpert)) - .build(); - - assertThat(wf.getDo()).hasSize(2); - assertThat(wf.getDo().get(0).getName()).isEqualTo("lineup-0"); - assertThat(wf.getDo().get(1).getName()).isEqualTo("lineup-1"); - wf.getDo() - .forEach( - ti -> { - assertThat(ti.getTask().getCallTask()).isNotNull(); - assertThat(ti.getTask().getCallTask().get()).isNotNull(); - }); - } - - @Test - void mixSpecAndAgent() { - Workflow wf = - AgentWorkflowBuilder.workflow("mixFlow") - .tasks( - d -> - d.set("init", s -> s.expr("$.mood = 'comedy'")) - .agent("pickMovies", newMovieExpert()) - .set("done", "$.done = true")) - .build(); - - assertThat(wf.getDo()).hasSize(3); - assertThat(wf.getDo().get(0).getTask().getSetTask()).isNotNull(); - assertThat(wf.getDo().get(1).getTask().getCallTask().get()).isNotNull(); - assertThat(wf.getDo().get(2).getTask().getSetTask()).isNotNull(); - } - - @Test - void loopOnlyAgents() { - Agents.MovieExpert expert = newMovieExpert(); - - Workflow wf = - AgentWorkflowBuilder.workflow().tasks(d -> d.loop(l -> l.subAgents(expert))).build(); - - assertNotNull(wf); - assertThat(wf.getDo()).hasSize(1); - - TaskItem ti = wf.getDo().get(0); - Task t = ti.getTask(); - assertThat(t.getForTask()).isInstanceOf(ForTaskFunction.class); - - ForTaskFunction fn = (ForTaskFunction) t.getForTask(); - assertNotNull(fn.getDo()); - assertThat(fn.getDo()).hasSize(1); - assertNotNull(fn.getDo().get(0).getTask().getCallTask().get()); - } - - @Test - void loopWithMaxIterationsAndExitCondition() { - Agents.MovieExpert expert = newMovieExpert(); - - AtomicInteger max = new AtomicInteger(4); - Predicate exit = - cog -> { - // stop when we already have at least one movie picked in state - var movies = cog.readState("movies", null); - return movies != null; - }; - - Workflow wf = - AgentWorkflowBuilder.workflow("loop-ctrl") - .tasks( - d -> - d.loop( - "refineMovies", - l -> - l.maxIterations(max.get()) - .exitCondition(exit) - .subAgents("picker", expert))) - .build(); - - TaskItem ti = wf.getDo().get(0); - ForTaskFunction fn = (ForTaskFunction) ti.getTask().getForTask(); - - assertNotNull(fn.getCollection(), "Synthetic collection should exist for maxIterations"); - assertNotNull(fn.getWhilePredicate(), "While predicate set from exitCondition"); - assertThat(fn.getDo()).hasSize(1); - } - - @Test - @DisplayName("parallel() creates one ForkTask with N callFn branches") - void parallelAgents() { - Agents.MovieExpert a1 = AgentsUtils.newMovieExpert(); - Agents.MovieExpert a2 = AgentsUtils.newMovieExpert(); - Agents.MovieExpert a3 = AgentsUtils.newMovieExpert(); - - Workflow wf = - AgentWorkflowBuilder.workflow("parallelFlow") - .tasks(d -> d.parallel("p", a1, a2, a3)) - .build(); - - assertThat(wf.getDo()).hasSize(1); - TaskItem top = wf.getDo().get(0); - Task task = top.getTask(); - assertThat(task.getForkTask()).isInstanceOf(ForkTask.class); - - ForkTask fork = task.getForkTask(); - assertThat(fork.getFork().getBranches()).hasSize(3); - - fork.getFork() - .getBranches() - .forEach( - branch -> { - assertThat(branch.getTask().getCallTask().get()).isInstanceOf(CallJava.class); - }); - } - - @Test - @DisplayName("workflow callFn(name,cfg) produces CallJava with no guard") - void testWorkflowCallFnBare() { - Workflow wf = - AgentWorkflowBuilder.workflow() - .tasks(d -> d.function("myCall", fn -> fn.function(ctx -> "hello"))) - .build(); - - assertThat(wf.getDo()).hasSize(1); - TaskItem ti = wf.getDo().get(0); - - assertInstanceOf(CallJava.class, ti.getTask().getCallTask().get()); - } - - @Test - @DisplayName("workflow callFn with Java DSL guard attaches predicate") - void testWorkflowCallFnWithPredicate() { - Predicate guard = cog -> true; - - Workflow wf = - AgentWorkflowBuilder.workflow() - .tasks(d -> d.function("guarded", fn -> fn.function(ctx -> "x").when(guard))) - .build(); - - TaskItem ti = wf.getDo().get(0); - assertInstanceOf(CallJava.class, ti.getTask().getCallTask().get()); - } - - @Test - @DisplayName("workflow loop with maxIterations only generates collection and no predicate") - void testWorkflowLoopMaxIterationsOnly() { - Agents.MovieExpert expert = AgentsUtils.newMovieExpert(); - - Workflow wf = - AgentWorkflowBuilder.workflow("maxFlow") - .tasks(d -> d.loop("limit", l -> l.maxIterations(2).subAgents("sub", expert))) - .build(); - - TaskItem ti = wf.getDo().get(0); - ForTaskFunction fn = (ForTaskFunction) ti.getTask().getForTask(); - - // synthetic collection is created - assertThat(fn.getCollection()).isNotNull(); - // no exitCondition → no whilePredicate set - assertNull(fn.getWhilePredicate(), "No while predicate when only maxIterations"); - // inner subAgents block still generates exactly one call branch - assertThat(fn.getDo()).hasSize(1); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java deleted file mode 100644 index 69a5cf77c..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgenticWorkflowHelperIT.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.AgentWorkflowBuilder.workflow; -import static io.serverlessworkflow.fluent.agentic.Agents.*; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.*; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAstrologyAgent; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newAudienceEditor; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newCreativeWriter; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newFoodExpert; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newMovieExpert; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleEditor; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newStyleScorer; -import static io.serverlessworkflow.fluent.agentic.AgentsUtils.newSummaryStory; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.conditional; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import dev.langchain4j.agentic.scope.AgenticScope; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.api.types.func.CallTaskJava; -import io.serverlessworkflow.impl.WorkflowApplication; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.IntStream; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public class AgenticWorkflowHelperIT { - - @Test - @DisplayName("Sequential agents via DSL.sequence(...)") - public void sequentialWorkflow() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - - NovelCreator novelCreator = - AgenticWorkflow.of(NovelCreator.class) - .flow(workflow("seqFlow").sequence(creativeWriter, audienceEditor, styleEditor)) - .build(); - - String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); - assertNotNull(story); - } - - @Test - @DisplayName("Parallel agents via DSL.parallel(...)") - public void parallelWorkflow() { - var foodExpert = newFoodExpert(); - var movieExpert = newMovieExpert(); - - Function> planEvening = - input -> { - List movies = (List) input.readState("movies"); - List meals = (List) input.readState("meals"); - - int max = Math.min(movies.size(), meals.size()); - return IntStream.range(0, max) - .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i))) - .toList(); - }; - - EveningPlannerAgent eveningPlannerAgent = - AgenticWorkflow.of(EveningPlannerAgent.class) - .flow(workflow("parallelFlow").parallel(foodExpert, movieExpert).outputAs(planEvening)) - .build(); - List result = eveningPlannerAgent.plan("romantic"); - assertEquals(3, result.size()); - } - - @Test - @DisplayName("Loop test with condition") - public void loopTest() { - var creativeWriter = newCreativeWriter(); - var styleScorer = newStyleScorer(); - var styleEditor = newStyleEditor(); - - Predicate until = s -> s.readState("score", 0.0) >= 0.8; - - StyledWriter styledWriter = - AgenticWorkflow.of(StyledWriter.class) - .flow(workflow("loopFlow").agent(creativeWriter).loop(until, styleScorer, styleEditor)) - .build(); - - String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy"); - assertNotNull(story); - } - - @Test - @DisplayName("Looping agents via DSL.loop(...)") - public void loopWorkflowWithMaxIterations() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - var summaryStory = newSummaryStory(); - - NovelCreator novelCreator = - AgenticWorkflow.of(NovelCreator.class) - .flow( - workflow("seqFlow") - .agent(creativeWriter) - .sequence(audienceEditor, styleEditor) - .agent(summaryStory)) - .build(); - - String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy"); - assertNotNull(story); - } - - @Test - @DisplayName("Human in the loop") - public void humanInTheLoop() { - var astrologyAgent = newAstrologyAgent(); - - var askSign = - new Function() { - @Override - public AgenticScope apply(AgenticScope holder) { - System.out.println("What's your star sign?"); - // var sign = System.console().readLine(); - holder.writeState("sign", "piscis"); - return holder; - } - }; - - String result = - AgenticWorkflow.of(HoroscopeAgent.class) - .flow( - workflow("humanInTheLoop") - .inputFrom(askSign) - // .tasks(tasks -> tasks.callFn(fn(askSign))) // TODO should work too - .agent(astrologyAgent)) - .build() - .invoke("My name is Mario. What is my horoscope?"); - - assertNotNull(result); - } - - @Test - @DisplayName("Error handling with agents") - public void errorHandling() { - var creativeWriter = newCreativeWriter(); - var audienceEditor = newAudienceEditor(); - var styleEditor = newStyleEditor(); - - Workflow wf = - workflow("seqFlow") - .sequence("process", creativeWriter, audienceEditor, styleEditor) - .build(); - - List items = wf.getDo(); - assertThat(items).hasSize(3); - - assertThat(items.get(0).getName()).isEqualTo("process-0"); - assertThat(items.get(1).getName()).isEqualTo("process-1"); - assertThat(items.get(2).getName()).isEqualTo("process-2"); - items.forEach(it -> assertThat(it.getTask().getCallTask()).isInstanceOf(CallTaskJava.class)); - - Map input = - Map.of( - "topic", "fantasy", - "style", "funny", - "audience", "young adults"); - - Map result = null; - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); - } catch (Exception e) { - throw new RuntimeException("Workflow execution failed", e); - } - - assertThat(result).containsKey("story"); - } - - @Test - @DisplayName("Conditional agents via choice(...)") - public void conditionalWorkflow() { - - var category = newCategoryRouter(); - var medicalExpert = newMedicalExpert(); - var technicalExpert = newTechnicalExpert(); - var legalExpert = newLegalExpert(); - - Workflow wf = - workflow("conditional") - .sequence("process", category) - .tasks( - doTasks( - conditional(RequestCategory.MEDICAL::equals, medicalExpert), - conditional(RequestCategory.TECHNICAL::equals, technicalExpert), - conditional(RequestCategory.LEGAL::equals, legalExpert))) - .build(); - - Map input = Map.of("request", "What is the best treatment for a common cold?"); - - Map result; - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - result = app.workflowDefinition(wf).instance(input).start().get().asMap().orElseThrow(); - } catch (Exception e) { - throw new RuntimeException("Workflow execution failed", e); - } - - assertThat(result).containsKey("response"); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java deleted file mode 100644 index 80323ee79..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ /dev/null @@ -1,408 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.agentic.Agent; -import dev.langchain4j.agentic.internal.AgentSpecification; -import dev.langchain4j.agentic.scope.AgenticScopeAccess; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import java.util.List; - -public interface Agents { - - interface ChatBot { - @UserMessage( - """ - You are a happy chat bot, reply to my message: - {{userInput}}. - """) - @Agent - String chat(@V("userInput") String userInput); - } - - interface MovieExpert { - - @UserMessage( - """ - You are a great evening planner. - Propose a list of 3 movies matching the given mood. - The mood is {{mood}}. - Provide a list with the 3 items and nothing else. - """) - @Agent - List findMovie(@V("mood") String mood); - } - - interface SettingAgent extends AgentSpecification { - - @UserMessage( - """ - Create a vivid {{style}} setting. It should include the time period, the state of technology, - key locations, and a brief description of the world’s political or social situation. - Make it imaginative, atmospheric, and suitable for a {{style}} novel. - """) - @Agent( - "Generates an imaginative setting including timeline, technology level, and world structure") - String invoke(@V("style") String style); - } - - interface HeroAgent extends AgentSpecification { - - @UserMessage( - """ - Invent a compelling protagonist for a {{style}} story. Describe their background, personality, - motivations, and any unique skills or traits. - """) - @Agent("Creates a unique and relatable protagonist with rich backstory and motivations.") - String invoke(@V("style") String style); - } - - interface ConflictAgent extends AgentSpecification { - - @UserMessage( - """ - Generate a central conflict or threat for a {{style}} plot. It can be external or - internal (e.g. moral dilemma, personal transformation). - Make it high-stakes and thematically rich. - """) - @Agent("Proposes a central conflict or dramatic tension to drive a compelling narrative.") - String invoke(@V("style") String style); - } - - interface FactAgent extends AgentSpecification { - - @UserMessage( - """ - Generate a unique sci-fi fact about an alien civilization's {{goal}} environment or evolutionary history. Make it imaginative and specific. - """) - @Agent("Generates a core fact that defines the foundation of an civilization.") - String invoke(@V("fact") String fact); - } - - interface CultureAgent extends AgentSpecification { - - @UserMessage( - """ - Given the following sci-fi fact about an civilization, describe 3–5 unique cultural traits, traditions, or societal structures that naturally emerge from this environment. - Fact: - {{fact}} - """) - @Agent("Derives cultural traits from the environmental/evolutionary fact.") - List invoke(@V("fact") String fact); - } - - interface TechnologyAgent extends AgentSpecification { - - @UserMessage( - """ - Given the following sci-fi fact about an alien civilization, describe 3–5 technologies or engineering solutions they might have developed. Focus on tools, transportation, communication, and survival systems. - Fact: - {{fact}} - """) - @Agent("Derives plausible technological inventions from the fact.") - List invoke(@V("fact") String fact); - } - - interface StorySeedAgent extends AgentSpecification { - - @UserMessage( - """ - You are a science fiction writer. Given the following title, come up with a short story premise. Describe the world, the central concept, and the thematic direction (e.g., dystopia, exploration, AI ethics). - Title: {{title}} - """) - @Agent("Generates a high-level sci-fi premise based on a title.") - String invoke(@V("title") String title); - } - - interface PlotAgent extends AgentSpecification { - - @UserMessage( - """ - Using the following premise, outline a three-act structure for a science fiction short story. Include a brief description of the main character, the inciting incident, the rising conflict, and the resolution. - Premise: - {{premise}} - """) - @Agent("Transforms a premise into a structured sci-fi plot.") - String invoke(@V("premise") String premise); - } - - interface SceneAgent extends AgentSpecification { - - @UserMessage( - """ - Write the opening scene of a science fiction short story based on the following plot outline. Introduce the main character and immerse the reader in the setting. Use vivid, cinematic language. - Plot: - {{plot}} - """) - @Agent("Generates the opening scene of the story from a plot outline.") - String invoke(@V("plot") String plot); - } - - interface MeetingInvitationDraft extends AgentSpecification { - - @UserMessage( - """ - You are a professional meeting invitation writer. Draft a concise and clear meeting invitation email based on the following details: - Subject: {{subject}} - Date: {{date}} - Time: {{time}} - Location: {{location}} - Agenda: {{agenda}} - """) - @Agent("Drafts a professional meeting invitation email.") - String invoke( - @V("subject") String subject, - @V("date") String date, - @V("time") String time, - @V("location") String location, - @V("agenda") String agenda); - } - - interface MeetingInvitationStyle extends AgentSpecification { - - @UserMessage( - """ - You are a professional meeting invitation writer. Rewrite the following meeting invitation email to better fit the {{style}} style: - Original Invitation: {{invitation}} - """) - @Agent("Edits a meeting invitation email to better fit a given style.") - String invoke(@V("invitation") String invitation, @V("style") String style); - } - - interface EmailDrafter { - - @UserMessage( - """ - You are a precise email drafting assistant. - - GOAL - - Draft a professional email that achieves the stated purpose. - - Keep it concise and skimmable. - - INPUT - recipient_name: {{recipientName}} - sender_name: {{senderName}} - purpose: {{purpose}} // e.g., follow-up, scheduling, proposal, apology, onboarding - key_points: {{keyPoints}} // bullet list or comma-separated facts - tone: {{tone}} // e.g., friendly, neutral, formal - length: {{length}} // short|medium - call_to_action: {{cta}} // e.g., "reply with a time", "confirm receipt", or empty - signature: {{signature}} // prebuilt block; do NOT invent - allowed_domains: {{allowedDomains}} // e.g., ["acme.com","example.com"] - known_links: {{links}} // URLs you may use; if not in allowed_domains, do not include - - HARD RULES - - Never fabricate facts, prices, or promises. - - Only include links from allowed_domains and only those listed in known_links. - - Do not include internal/confidential URLs. - - If you lack a detail, write a neutral placeholder (e.g., "[DATE]"). - - Keep subject <= 60 characters if possible. - - One clear CTA max. - - OUTPUT - Return ONLY a compact JSON object with keys: - { - "subject": "...", - "body_plain": "...", - "links": ["..."] // subset of known_links, or empty - } - No markdown, no explanations, no extra text. - """) - @Agent("Drafts a new outbound email from structured inputs; returns JSON.") - String draftNew( - @V("recipientName") String recipientName, - @V("senderName") String senderName, - @V("purpose") String purpose, - @V("keyPoints") List keyPoints, - @V("tone") String tone, - @V("length") String length, - @V("cta") String cta, - @V("signature") String signature, - @V("allowedDomains") List allowedDomains, - @V("links") List links); - } - - interface CreativeWriter { - - @UserMessage( - """ - You are a creative writer. - Generate a draft of a story no more than - 3 sentences long around the given topic. - Return only the story and nothing else. - The topic is {{topic}}. - """) - @Agent("Generates a story based on the given topic") - String generateStory(@V("topic") String topic); - } - - interface AudienceEditor { - - @UserMessage( - """ - You are a professional editor. - Analyze and rewrite the following story to better align - with the target audience of {{audience}}. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edits a story to better fit a given audience") - String editStory(@V("story") String story, @V("audience") String audience); - } - - interface StyleEditor { - - @UserMessage( - """ - You are a professional editor. - Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edits a story to better fit a given style") - String editStory(@V("story") String story, @V("style") String style); - } - - interface StyleScorer { - - @UserMessage( - """ - You are a critical reviewer. - Give a review score between 0.0 and 1.0 for the following - story based on how well it aligns with the style '{{style}}'. - Return only the score and nothing else. - - The story is: "{{story}}" - """) - @Agent("Scores a story based on how well it aligns with a given style") - double scoreStyle(@V("story") String story, @V("style") String style); - } - - interface StyledWriter extends AgenticScopeAccess { - - @Agent - String writeStoryWithStyle(@V("topic") String topic, @V("style") String style); - } - - interface SummaryStory { - - @UserMessage( - """ - Return only the summary text and nothing else. - """) - @Agent("Edits a story to better fit a given audience") - String summaryStory(@V("story") String story); - } - - interface FoodExpert { - - @UserMessage( - """ - You are a great evening planner. - Propose a list of 3 meals matching the given mood. - The mood is {{mood}}. - For each meal, just give the name of the meal. - Provide a list with the 3 items and nothing else. - """) - @Agent - List findMeal(@V("mood") String mood); - } - - interface AstrologyAgent { - @SystemMessage( - """ - You are an astrologist that generates horoscopes based on the user's name and zodiac sign. - """) - @UserMessage( - """ - Generate the horoscope for {{name}} who is a {{sign}}. - """) - @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") - String horoscope(@V("name") String name, @V("sign") String sign); - } - - interface HoroscopeAgent { - - @Agent - String invoke(@V("name") String name); - } - - enum RequestCategory { - LEGAL, - MEDICAL, - TECHNICAL, - UNKNOWN - } - - interface CategoryRouter { - - @UserMessage( - """ - Analyze the following user request and categorize it as 'legal', 'medical' or 'technical'. - In case the request doesn't belong to any of those categories categorize it as 'unknown'. - Reply with only one of those words and nothing else. - The user request is: '{{request}}'. - """) - @Agent("Categorizes a user request") - RequestCategory classify(@V("request") String request); - } - - interface MedicalExpert { - - @dev.langchain4j.service.UserMessage( - """ - You are a medical expert. - Analyze the following user request under a medical point of view and provide the best possible answer. - The user request is {{it}}. - """) - @Agent("A medical expert") - String medicalRequest(String request); - } - - interface LegalExpert { - - @dev.langchain4j.service.UserMessage( - """ - You are a legal expert. - Analyze the following user request under a legal point of view and provide the best possible answer. - The user request is {{it}}. - """) - @Agent("A legal expert") - String legalRequest(String request); - } - - interface TechnicalExpert { - - @dev.langchain4j.service.UserMessage( - """ - You are a technical expert. - Analyze the following user request under a technical point of view and provide the best possible answer. - The user request is {{it}}. - """) - @Agent("A technical expert") - String technicalRequest(String request); - } - - record EveningPlan(String movie, String meal) {} - - interface EveningPlannerAgent { - - @Agent - List plan(@V("mood") String mood); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java deleted file mode 100644 index 2404d1c12..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; -import static org.mockito.Mockito.spy; - -import dev.langchain4j.agentic.Agent; -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.service.V; - -public final class AgentsUtils { - - private AgentsUtils() {} - - public static Agents.MovieExpert newMovieExpert() { - return spy( - AgenticServices.agentBuilder(Agents.MovieExpert.class) - .outputKey("movies") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.MovieExpert newMovieExpert2() { - return spy( - AgenticServices.agentBuilder(Agents.MovieExpert.class) - .outputKey("movies") - .name("findMovie2") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.CreativeWriter newCreativeWriter() { - return spy( - AgenticServices.agentBuilder(Agents.CreativeWriter.class) - .outputKey("story") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.AudienceEditor newAudienceEditor() { - return spy( - AgenticServices.agentBuilder(Agents.AudienceEditor.class) - .outputKey("story") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.StyleEditor newStyleEditor() { - return spy( - AgenticServices.agentBuilder(Agents.StyleEditor.class) - .outputKey("story") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.SummaryStory newSummaryStory() { - return spy( - AgenticServices.agentBuilder(Agents.SummaryStory.class) - .outputKey("story") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.StyleScorer newStyleScorer() { - return spy( - AgenticServices.agentBuilder(Agents.StyleScorer.class) - .outputKey("score") - .chatModel(BASE_MODEL) - .build()); - } - - public static Agents.FoodExpert newFoodExpert() { - return spy( - AgenticServices.agentBuilder(Agents.FoodExpert.class) - .chatModel(BASE_MODEL) - .outputKey("meals") - .build()); - } - - public static Agents.AstrologyAgent newAstrologyAgent() { - return spy( - AgenticServices.agentBuilder(Agents.AstrologyAgent.class) - .chatModel(BASE_MODEL) - .outputKey("horoscope") - .build()); - } - - public static Agents.CategoryRouter newCategoryRouter() { - return spy( - AgenticServices.agentBuilder(Agents.CategoryRouter.class) - .chatModel(BASE_MODEL) - .outputKey("category") - .build()); - } - - public static Agents.MedicalExpert newMedicalExpert() { - return spy( - AgenticServices.agentBuilder(Agents.MedicalExpert.class) - .chatModel(BASE_MODEL) - .outputKey("response") - .build()); - } - - public static Agents.TechnicalExpert newTechnicalExpert() { - return spy( - AgenticServices.agentBuilder(Agents.TechnicalExpert.class) - .chatModel(BASE_MODEL) - .outputKey("response") - .build()); - } - - public static Agents.LegalExpert newLegalExpert() { - return spy( - AgenticServices.agentBuilder(Agents.LegalExpert.class) - .chatModel(BASE_MODEL) - .outputKey("response") - .build()); - } - - public interface NovelCreator { - - @Agent - String createNovel( - @V("topic") String topic, @V("audience") String audience, @V("style") String style); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java deleted file mode 100644 index 717f84944..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/ChatBotIT.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.spy; - -import com.fasterxml.jackson.databind.ObjectMapper; -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.memory.chat.MessageWindowChatMemory; -import io.cloudevents.CloudEvent; -import io.cloudevents.jackson.JsonCloudEventData; -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 io.serverlessworkflow.impl.events.InMemoryEvents; -import io.serverlessworkflow.impl.jackson.JsonUtils; -import java.util.Map; -import java.util.Objects; -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.Test; - -public class ChatBotIT { - - @Test - @SuppressWarnings("unchecked") - void chat_bot() { - final ObjectMapper mapper = new ObjectMapper(); - Agents.ChatBot chatBot = - spy( - AgenticServices.agentBuilder(Agents.ChatBot.class) - .chatModel(Models.BASE_MODEL) - .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) - .outputKey("conversation") - .build()); - BlockingQueue replyEvents = new LinkedBlockingQueue<>(); - BlockingQueue finishedEvents = new LinkedBlockingQueue<>(); - - // 1. listen to an event containing `message` key in the body - // 2. if contains, call the agent, if not end the workflow - // 3. After replying to the chat, return - final Workflow listenWorkflow = - AgentWorkflowBuilder.workflow("chat-bot") - .tasks( - t -> - t.listen( - l -> - l.to( - to -> - to.any( - c -> - c.with( - event -> - event.type( - "org.acme.chatbot.request"))) - .until( - until -> - until.one( - one -> - one.with( - e -> - e.type( - "org.acme.chatbot.finalize"))))) - .forEach( - f -> - f.tasks( - tasks -> - tasks - .agent(chatBot) - .emit( - emit -> - emit.event( - e -> - e.type( - "org.acme.chatbot.reply") - .data( - convo -> { - var node = - JsonUtils - .object() - .put( - "conversation", - convo - .getOrDefault( - "conversation", - "") - .toString()); - return JsonCloudEventData - .wrap(node); - }, - Map.class)))))) - .emit(emit -> emit.event(e -> e.type("org.acme.chatbot.finished")))) - .build(); - - InMemoryEvents eventBroker = new InMemoryEvents(); - eventBroker.register("org.acme.chatbot.reply", ce -> replyEvents.add((CloudEvent) ce)); - eventBroker.register("org.acme.chatbot.finished", ce -> finishedEvents.add((CloudEvent) ce)); - - try (WorkflowApplication app = - WorkflowApplication.builder() - .withEventConsumer(eventBroker) - .withEventPublisher(eventBroker) - .build()) { - final WorkflowInstance waitingInstance = - app.workflowDefinition(listenWorkflow).instance(Map.of()); - final CompletableFuture runningModel = waitingInstance.start(); - - // The workflow is just waiting for the event - assertEquals(WorkflowStatus.WAITING, waitingInstance.status()); - - // Publish the events - - eventBroker.publish(newRequestMessage("Hi! Can you tell me a good duck joke?")); - - CloudEvent reply = replyEvents.poll(60, TimeUnit.SECONDS); - assertNotNull(reply); - - eventBroker.publish(newRequestMessage("Oh I didn't like this one, please tell me another.")); - reply = replyEvents.poll(60, TimeUnit.SECONDS); - assertNotNull(reply); - assertThat( - ((JsonCloudEventData) Objects.requireNonNull(reply.getData())) - .getNode() - .get("conversation") - .asText()) - .contains("No worries"); - - // Empty message completes the workflow - eventBroker.publish(newFinalizeMessage()); - CloudEvent finished = finishedEvents.poll(60, TimeUnit.SECONDS); - assertNotNull(finished); - assertThat(finishedEvents).isEmpty(); - - assertThat(runningModel).isCompleted(); - assertEquals(WorkflowStatus.COMPLETED, waitingInstance.status()); - - } catch (InterruptedException e) { - fail(e.getMessage()); - } - } - - private CloudEvent newRequestMessage(String message) { - return CloudEventsTestBuilder.newMessage( - String.format("{\"userInput\": \"%s\"}", message), "org.acme.chatbot.request"); - } - - private CloudEvent newFinalizeMessage() { - return CloudEventsTestBuilder.newMessage("", "org.acme.chatbot.finalize"); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/CloudEventsTestBuilder.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/CloudEventsTestBuilder.java deleted file mode 100644 index e2fec1e7e..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/CloudEventsTestBuilder.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import io.cloudevents.CloudEvent; -import io.cloudevents.core.v1.CloudEventBuilder; -import java.net.URI; -import java.time.OffsetDateTime; -import java.util.UUID; - -public final class CloudEventsTestBuilder { - - private CloudEventsTestBuilder() {} - - public static CloudEvent newMessage(String data, String type) { - if (data == null) { - data = ""; - } - return new CloudEventBuilder() - .withData(data.getBytes()) - .withType(type) - .withId(UUID.randomUUID().toString()) - .withDataContentType("application/json") - .withSource(URI.create("test://localhost")) - .withSubject("A chatbot message") - .withTime(OffsetDateTime.now()) - .build(); - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java deleted file mode 100644 index e0eb6e544..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/EmailDrafterIT.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.cases; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.event; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.fn; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.toAny; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -import dev.langchain4j.agentic.AgenticServices; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.data.PojoCloudEventData; -import io.serverlessworkflow.api.types.EventFilter; -import io.serverlessworkflow.api.types.EventProperties; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL; -import io.serverlessworkflow.impl.WorkflowApplication; -import io.serverlessworkflow.impl.WorkflowStatus; -import io.serverlessworkflow.impl.jackson.JsonUtils; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import org.acme.EmailDraft; -import org.acme.EmailDrafts; -import org.acme.EmailPolicies; -import org.acme.PolicyDecision; -import org.junit.jupiter.api.Test; - -public class EmailDrafterIT { - - @Test - @SuppressWarnings("unchecked") - void email_drafter_agent() { - Agents.EmailDrafter emailDrafter = - AgenticServices.agentBuilder(Agents.EmailDrafter.class) - .chatModel(Models.BASE_MODEL) - .outputKey("email_draft") - .build(); - - BlockingQueue finishedEvents = new LinkedBlockingQueue<>(); - - final Workflow emailDrafterWorkflow = - AgentWorkflowBuilder.workflow("emailDrafterAgentic") - .tasks( - tasks -> - tasks - .agent("agentEmailDrafter", emailDrafter) - .function("parseDraft", fn(EmailDrafts::parse, String.class)) - .function("policyCheck", fn(EmailPolicies::policyCheck, EmailDraft.class)) - .switchCase( - "needsHumanReview?", - cases( - AgenticDSL.caseOf( - d -> !EmailPolicies.Decision.AUTO_SEND.equals(d.decision()), - PolicyDecision.class) - .then("requestReview"), - AgenticDSL.caseDefault("emailFinished"))) - .emit( - "requestReview", - event( - "org.acme.email.review.required", - payload -> - PojoCloudEventData.wrap( - payload, - p -> - JsonUtils.mapper() - .writeValueAsString(payload) - .getBytes()), - PolicyDecision.class)) - .listen( - "waitForReview", - toAny("org.acme.email.approved", "org.acme.email.denied")) - .emit("emailFinished", event("org.acme.email.finished", null))) - .build(); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - // input - Map emailVars = - Map.ofEntries( - Map.entry("recipientName", "John Mars"), - Map.entry("senderName", "Rick Venus"), - Map.entry("purpose", "follow-up"), - Map.entry( - "keyPoints", - List.of( - "Thanks for the call yesterday", - "Attaching the one-page overview", - "Available Wed or Thu 2–4pm ET", - "Check the links for more")), - Map.entry("tone", "friendly"), // friendly | neutral | formal - Map.entry("length", "short"), // short | medium - Map.entry("cta", "Please reply with a 15-minute slot this week."), - Map.entry("signature", "Best regards,\nRick Venus\nEngineer\nAcme"), - Map.entry("allowedDomains", List.of("acme.com", "example.com")), - Map.entry( - "links", - List.of( - "https://acme.com/proposals/alpha", "https://example.com/schedule/rick"))); - - // Listen to de event - app.eventConsumer() - .register( - app.eventConsumer() - .listen( - new EventFilter() - .withWith(new EventProperties().withType("org.acme.email.finished")), - app), - ce -> finishedEvents.add((CloudEvent) ce)); - - var instance = app.workflowDefinition(emailDrafterWorkflow).instance(emailVars); - var running = instance.start().join(); - var policyDecision = running.as(PolicyDecision.class); - assertThat(policyDecision).isNotNull(); - assertThat(policyDecision.isPresent()).isTrue(); - assertThat(policyDecision.get().decision()).isEqualTo(EmailPolicies.Decision.AUTO_SEND); - assertThat(instance.status()).isEqualTo(WorkflowStatus.COMPLETED); - - CloudEvent finished = finishedEvents.poll(1, TimeUnit.SECONDS); - assertThat(finished).isNotNull(); - } catch (InterruptedException e) { - fail(e); - } - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/MixedWorkflowIT.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/MixedWorkflowIT.java deleted file mode 100644 index a91cfe967..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/MixedWorkflowIT.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.agent; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.doTasks; -import static io.serverlessworkflow.fluent.agentic.dsl.AgenticDSL.function; -import static org.junit.jupiter.api.Assertions.assertFalse; -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.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.impl.WorkflowApplication; -import io.serverlessworkflow.impl.WorkflowModel; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Test; - -public class MixedWorkflowIT { - /** - * 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)) - .outputKey("userInput") - .build()); - - final Workflow mixedWorkflow = - AgentWorkflowBuilder.workflow("chat-bot") - .tasks( - doTasks( - function(input -> Map.of("userInput", input), String.class), - agent(chatBot), - // 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) - function(input -> "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")); - } - } -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java deleted file mode 100644 index 170281c11..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import dev.langchain4j.model.chat.ChatModel; -import dev.langchain4j.model.ollama.OllamaChatModel; -import java.time.Duration; - -public class Models { - static final ChatModel BASE_MODEL = - OllamaChatModel.builder() - .baseUrl("http://127.0.0.1:11434") - .modelName("qwen2.5:7b") - .timeout(Duration.ofMinutes(10)) - .temperature(0.0) - .logRequests(true) - .logResponses(true) - .build(); -} diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md deleted file mode 100644 index 21b5fade9..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/README.md +++ /dev/null @@ -1,406 +0,0 @@ -# Implementation of Scenarios from [LangChain4j Agents Tutorials](https://docs.langchain4j.dev/tutorials/agents/) for CNCF Workflow Java DSL - -# Sequential workflow -### Common part: -
Click to expand - -```java -public interface AudienceEditor { - - @UserMessage(""" - You are a professional editor. - Analyze and rewrite the following story to better align - with the target audience of {{audience}}. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edits a story to better fit a given audience") - String editStory(@V("story") String story, @V("audience") String audience); -} - -interface CreativeWriter { - - @UserMessage(""" - You are a creative writer. - Generate a draft of a story no more than - 3 sentences long around the given topic. - Return only the story and nothing else. - The topic is {{topic}}. - """) - @Agent("Generates a story based on the given topic") - String generateStory(@V("topic") String topic); -} - -public interface StyleEditor { - - @UserMessage(""" - You are a professional editor. - Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edits a story to better fit a given style") - String editStory(@V("story") String story, @V("style") String style); -} - -CreativeWriter creativeWriter = AgenticServices - .agentBuilder(CreativeWriter.class) - .chatModel(BASE_MODEL) - .outputName("story") - .build(); - -AudienceEditor audienceEditor = AgenticServices - .agentBuilder(AudienceEditor.class) - .chatModel(BASE_MODEL) - .outputName("story") - .build(); - -StyleEditor styleEditor = AgenticServices - .agentBuilder(StyleEditor.class) - .chatModel(BASE_MODEL) - .outputName("story") - .build(); - -Map input = Map.of( - "topic", "dragons and wizards", - "style", "fantasy", - "audience", "young adults" -); -``` -
- - - - - - - - - - - -
LangChain4jServerless Workflow
-
-UntypedAgent novelCreator = AgenticServices
-    .sequenceBuilder()
-    .subAgents(creativeWriter, audienceEditor, styleEditor)
-    .outputName("story")
-    .build();
-
-String story = (String) novelCreator.invoke(input);
-
-
-
-
-
-AgentsUtils.NovelCreator novelCreator = AgenticWorkflow.of(NovelCreator.class)
-            .flow(workflow("seqFlow")
-            .sequence(creativeWriter, audienceEditor, styleEditor))
-            .build();
- 
- 
-String story = novelCreator.createNovel("dragons and wizards", "young adults", "fantasy");
-
-
-
-
- - -### Loop workflow -### Common part: -
Click to expand - -```java - interface StyleEditor { - - @UserMessage( - """ - You are a professional editor. - Analyze and rewrite the following story to better fit and be more coherent with the {{style}} style. - Return only the story and nothing else. - The story is "{{story}}". - """) - @Agent("Edits a story to better fit a given style") - String editStory(@V("story") String story, @V("style") String style); -} - -interface StyleScorer { - - @UserMessage( - """ - You are a critical reviewer. - Give a review score between 0.0 and 1.0 for the following - story based on how well it aligns with the style '{{style}}'. - Return only the score and nothing else. - - The story is: "{{story}}" - """) - @Agent("Scores a story based on how well it aligns with a given style") - double scoreStyle(@V("story") String story, @V("style") String style); -} - -StyleEditor styleEditor = AgenticServices - .agentBuilder(StyleEditor.class) - .chatModel(BASE_MODEL) - .outputName("story") - .build(); - -StyleScorer styleScorer = AgenticServices - .agentBuilder(StyleScorer.class) - .chatModel(BASE_MODEL) - .outputName("score") - .build(); - - -``` - -
- - - - - - - - - - - -
LangChain4jServerless Workflow
-
-UntypedAgent styleReviewLoop = AgenticServices
-        .loopBuilder()
-        .subAgents(styleScorer, styleEditor)
-        .maxIterations(5)
-        .exitCondition(agenticScope -> agenticScope.readState("score", 0.0) >= 0.8)
-        .build();
-
-StyledWriter styledWriter = AgenticServices
-        .sequenceBuilder(StyledWriter.class)
-        .subAgents(creativeWriter, styleReviewLoop)
-        .outputName("story")
-        .build();
-
-String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
-
-
-
-
-
-Predicate until = s -> s.readState("score", 0.0) >= 0.8;
-
-StyledWriter styledWriter = AgenticWorkflow.of(StyledWriter.class)
-            .flow(workflow("loopFlow")
-            .agent(creativeWriter)
-            .loop(until, styleScorer, styleEditor))
-            .build();
- 
- 
- 
- 
- 
-
-String story = styledWriter.writeStoryWithStyle("dragons and wizards", "comedy");
-
-
-
-
- -### Parallel workflow -### Common part: -
Click to expand - -```java -public interface FoodExpert { - - @UserMessage(""" - You are a great evening planner. - Propose a list of 3 meals matching the given mood. - The mood is {{mood}}. - For each meal, just give the name of the meal. - Provide a list with the 3 items and nothing else. - """) - @Agent - List findMeal(@V("mood") String mood); -} - -public interface MovieExpert { - - @UserMessage(""" - You are a great evening planner. - Propose a list of 3 movies matching the given mood. - The mood is {mood}. - Provide a list with the 3 items and nothing else. - """) - @Agent - List findMovie(@V("mood") String mood); -} - -FoodExpert foodExpert = AgenticServices - .agentBuilder(FoodExpert.class) - .chatModel(BASE_MODEL) - .outputName("meals") - .build(); - -MovieExpert movieExpert = AgenticServices - .agentBuilder(MovieExpert.class) - .chatModel(BASE_MODEL) - .outputName("movies") - .build(); -``` -
- - - - - - - - - - - - -
LangChain4jServerless Workflow
-
-EveningPlannerAgent eveningPlannerAgent = AgenticServices
-    .parallelBuilder(EveningPlannerAgent.class)
-    .subAgents(foodExpert, movieExpert)
-    .executor(Executors.newFixedThreadPool(2))
-    .outputName("plans")
-    .output(agenticScope -> {
-        List movies = agenticScope.readState("movies", List.of());
-        List meals = agenticScope.readState("meals", List.of());
-        List moviesAndMeals = new ArrayList<>();
-        for (int i = 0; i < movies.size(); i++) {
-        if (i >= meals.size()) {
-            break;
-        }
-        moviesAndMeals.add(new EveningPlan(movies.get(i), meals.get(i)));
-        }
-        return moviesAndMeals;
-    })
-    .build();
-
-List plans = eveningPlannerAgent.plan("romantic");
-
-
-
-
-
-Function<AgenticScope, List> planEvening = input -> {
-  List movies = (List) input.readState("movies");
-  List meals = (List)  input.readState("meals");
-  int max = Math.min(movies.size(), meals.size());
-  return IntStream.range(0, max)
-     .mapToObj(i -> new EveningPlan(movies.get(i), meals.get(i)))
-     .toList();
-};
-
-EveningPlannerAgent eveningPlannerAgent = AgenticWorkflow.of(EveningPlannerAgent.class)
-            .flow(workflow("parallelFlow")
-            .parallel(foodExpert, movieExpert)
-            .outputAs(planEvening))
-            .build();
- 
- 
- 
- 
- 
-List result = eveningPlannerAgent.plan("romantic");
-
-
-
- - -### Human-in-the-loop -### Common part: -
Click to expand - -```java -public interface AstrologyAgent { - @SystemMessage(""" - You are an astrologist that generates horoscopes based on the user's name and zodiac sign. - """) - @UserMessage(""" - Generate the horoscope for {{name}} who is a {{sign}}. - """) - @Agent("An astrologist that generates horoscopes based on the user's name and zodiac sign.") - String horoscope(@V("name") String name, @V("sign") String sign); -} - -AstrologyAgent astrologyAgent = AgenticServices - .agentBuilder(AstrologyAgent.class) - .chatModel(BASE_MODEL) - .build(); -``` -
- - - - - - - - - - - -
LangChain4jServerless Workflow
-
-public record HumanInTheLoop(Consumer requestWriter, Supplier responseReader) {
-@Agent("An agent that asks the user for missing information")
-    public String askUser(String request) {
-        requestWriter.accept(request);
-        return responseReader.get();
-    }
-}
-
-HumanInTheLoop humanInTheLoop = AgenticServices.humanInTheLoopBuilder()
-  .description("An agent that asks the zodiac sign of the user")
-  .outputName("sign")
-  .requestWriter(request -> {
-     System.out.println(request);
-     System.out.print("> ");
-  }).responseReader(() -> System.console().readLine())
-  .build();
-
-SupervisorAgent horoscopeAgent = AgenticServices
-        .supervisorBuilder()
-        .chatModel(PLANNER_MODEL)
-        .subAgents(astrologyAgent, humanInTheLoop)
-        .build();
-
-horoscopeAgent.invoke("My name is Mario. What is my horoscope?")
-
-
-
-
-
-var askSign = new Function<AgenticScope, AgenticScope>() {
-  @Override
-  public AgenticScope apply(AgenticScope holder) {
-    System.out.println("What's your star sign?");
-    // var sign = System.console().readLine();
-    holder.writeState("sign", "piscis");
-    return holder;
-  }
-};
-
- 
- 
- 
- 
- 
- 
- 
-
-String result = AgenticWorkflow.of(HoroscopeAgent.class)
-  .flow(workflow("humanInTheLoop")
-  .inputFrom(askSign)
-  .agent(astrologyAgent))
-  .build()
-  .invoke("My name is Mario. What is my horoscope?");
-
-
-
\ No newline at end of file diff --git a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java b/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java deleted file mode 100644 index ed834ded1..000000000 --- a/experimental/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2020-Present The Serverless Workflow Specification Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.serverlessworkflow.fluent.agentic; - -import static io.serverlessworkflow.fluent.agentic.Agents.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import dev.langchain4j.agentic.AgenticServices; -import dev.langchain4j.agentic.scope.AgenticScope; -import dev.langchain4j.agentic.workflow.HumanInTheLoop; -import io.serverlessworkflow.api.types.Workflow; -import io.serverlessworkflow.impl.WorkflowApplication; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -class WorkflowTests { - - @Test - public void testAgent() throws ExecutionException, InterruptedException { - final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); - - when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); - when(storySeedAgent.outputKey()).thenReturn("premise"); - when(storySeedAgent.name()).thenReturn("storySeedAgent"); - - Workflow workflow = - AgentWorkflowBuilder.workflow("storyFlow") - .tasks(d -> d.agent("story", storySeedAgent)) - .build(); - - Map topic = new HashMap<>(); - topic.put("title", "A Great Story"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(topic) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals("storySeedAgent", result.readState("premise")); - } - } - - @Test - public void testAgents() throws ExecutionException, InterruptedException { - final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); - final PlotAgent plotAgent = mock(PlotAgent.class); - final SceneAgent sceneAgent = mock(SceneAgent.class); - - when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); - when(storySeedAgent.outputKey()).thenReturn("premise"); - when(storySeedAgent.name()).thenReturn("storySeedAgent"); - - when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent"); - when(plotAgent.outputKey()).thenReturn("plot"); - when(plotAgent.name()).thenReturn("plotAgent"); - - when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent"); - when(sceneAgent.outputKey()).thenReturn("story"); - when(sceneAgent.name()).thenReturn("sceneAgent"); - - Workflow workflow = - AgentWorkflowBuilder.workflow("storyFlow") - .tasks( - d -> - d.agent("story", storySeedAgent) - .agent("plot", plotAgent) - .agent("scene", sceneAgent)) - .build(); - - Map topic = new HashMap<>(); - topic.put("title", "A Great Story"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(topic) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals("sceneAgent", result.readState("story")); - } - } - - @Test - public void testSequence() throws ExecutionException, InterruptedException { - final StorySeedAgent storySeedAgent = mock(StorySeedAgent.class); - final PlotAgent plotAgent = mock(PlotAgent.class); - final SceneAgent sceneAgent = mock(SceneAgent.class); - - when(storySeedAgent.invoke(eq("A Great Story"))).thenReturn("storySeedAgent"); - when(storySeedAgent.outputKey()).thenReturn("premise"); - when(storySeedAgent.name()).thenReturn("storySeedAgent"); - - when(plotAgent.invoke(eq("storySeedAgent"))).thenReturn("plotAgent"); - when(plotAgent.outputKey()).thenReturn("plot"); - when(plotAgent.name()).thenReturn("plotAgent"); - - when(sceneAgent.invoke(eq("plotAgent"))).thenReturn("sceneAgent"); - when(sceneAgent.outputKey()).thenReturn("story"); - when(sceneAgent.name()).thenReturn("sceneAgent"); - - Workflow workflow = - AgentWorkflowBuilder.workflow("storyFlow") - .tasks(d -> d.sequence("story", storySeedAgent, plotAgent, sceneAgent)) - .build(); - - Map topic = new HashMap<>(); - topic.put("title", "A Great Story"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(topic) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals("sceneAgent", result.readState("story")); - } - } - - @Test - public void testParallel() throws ExecutionException, InterruptedException { - - final SettingAgent setting = mock(SettingAgent.class); - final HeroAgent hero = mock(HeroAgent.class); - final ConflictAgent conflict = mock(ConflictAgent.class); - - when(setting.invoke(eq("sci-fi"))).thenReturn("Fake conflict response"); - when(setting.outputKey()).thenReturn("setting"); - when(setting.name()).thenReturn("setting"); - - when(hero.invoke(eq("sci-fi"))).thenReturn("Fake hero response"); - when(hero.outputKey()).thenReturn("hero"); - when(hero.name()).thenReturn("hero"); - - when(conflict.invoke(eq("sci-fi"))).thenReturn("Fake setting response"); - when(conflict.outputKey()).thenReturn("conflict"); - when(conflict.name()).thenReturn("conflict"); - - Workflow workflow = - AgentWorkflowBuilder.workflow("parallelFlow") - .tasks(d -> d.parallel("story", setting, hero, conflict)) - .build(); - - Map topic = new HashMap<>(); - topic.put("style", "sci-fi"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - Map result = - app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); - - assertEquals("Fake conflict response", result.get("setting").toString()); - assertEquals("Fake hero response", result.get("hero").toString()); - assertEquals("Fake setting response", result.get("conflict").toString()); - } - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(topic) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals("Fake conflict response", result.readState("setting").toString()); - assertEquals("Fake hero response", result.readState("hero").toString()); - assertEquals("Fake setting response", result.readState("conflict").toString()); - } - } - - @Test - public void testSeqAndThenParallel() throws ExecutionException, InterruptedException { - final FactAgent factAgent = mock(FactAgent.class); - final CultureAgent cultureAgent = mock(CultureAgent.class); - final TechnologyAgent technologyAgent = mock(TechnologyAgent.class); - - List cultureTraits = - List.of("Alien Culture Trait 1", "Alien Culture Trait 2", "Alien Culture Trait 3"); - - List technologyTraits = - List.of("Alien Technology Trait 1", "Alien Technology Trait 2", "Alien Technology Trait 3"); - - when(factAgent.invoke(eq("alien"))).thenReturn("Some Fact about aliens"); - when(factAgent.outputKey()).thenReturn("fact"); - when(factAgent.name()).thenReturn("fact"); - - when(cultureAgent.invoke(eq("Some Fact about aliens"))).thenReturn(cultureTraits); - when(cultureAgent.outputKey()).thenReturn("culture"); - when(cultureAgent.name()).thenReturn("culture"); - - when(technologyAgent.invoke(eq("Some Fact about aliens"))).thenReturn(technologyTraits); - when(technologyAgent.outputKey()).thenReturn("technology"); - when(technologyAgent.name()).thenReturn("technology"); - Workflow workflow = - AgentWorkflowBuilder.workflow("alienCultureFlow") - .tasks( - d -> - d.sequence("fact", factAgent) - .parallel("cultureAndTechnology", cultureAgent, technologyAgent)) - .build(); - - Map topic = new HashMap<>(); - topic.put("fact", "alien"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - Map result = - app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); - - assertEquals(cultureTraits, result.get("culture")); - assertEquals(technologyTraits, result.get("technology")); - } - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(topic) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals(cultureTraits, result.readState("culture")); - assertEquals(technologyTraits, result.readState("technology")); - } - } - - @Test - @Disabled( - "HumanInTheLoop is not a dev.langchain4j.agentic.internal.AgentSpecification, we should treat it differently once it's implemented") - public void humanInTheLoop() throws ExecutionException, InterruptedException { - final MeetingInvitationDraft meetingInvitationDraft = mock(MeetingInvitationDraft.class); - when(meetingInvitationDraft.invoke( - eq("Meeting with John Doe"), - eq("2023-10-01"), - eq("08:00AM"), - eq("London"), - eq("Discuss project updates"))) - .thenReturn("Drafted meeting invitation for John Doe"); - when(meetingInvitationDraft.outputKey()).thenReturn("draft"); - when(meetingInvitationDraft.name()).thenReturn("draft"); - - final MeetingInvitationStyle meetingInvitationStyle = mock(MeetingInvitationStyle.class); - when(meetingInvitationStyle.invoke(eq("Drafted meeting invitation for John Doe"), eq("formal"))) - .thenReturn("Styled meeting invitation for John Doe"); - when(meetingInvitationStyle.outputKey()).thenReturn("styled"); - when(meetingInvitationStyle.name()).thenReturn("styled"); - - AtomicReference request = new AtomicReference<>(); - - HumanInTheLoop humanInTheLoop = - AgenticServices.humanInTheLoopBuilder() - .description( - "What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)") - .inputKey("style") - .outputKey("style") - .requestWriter( - q -> - request.set( - "What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)")) - .responseReader(() -> "formal") - .build(); - - Workflow workflow = - AgentWorkflowBuilder.workflow("meetingInvitationFlow") - .tasks( - d -> - d.sequence( - "draft", meetingInvitationDraft, humanInTheLoop, meetingInvitationStyle)) - .build(); - Map initialValues = new HashMap<>(); - initialValues.put("title", "Meeting with John Doe"); - initialValues.put("date", "2023-10-01"); - initialValues.put("time", "08:00AM"); - initialValues.put("location", "London"); - initialValues.put("agenda", "Discuss project updates"); - - try (WorkflowApplication app = WorkflowApplication.builder().build()) { - AgenticScope result = - app.workflowDefinition(workflow) - .instance(initialValues) - .start() - .get() - .as(AgenticScope.class) - .orElseThrow(); - - assertEquals("Styled meeting invitation for John Doe", result.readState("styled")); - } - } -} diff --git a/experimental/fluent/agentic/src/test/java/org/acme/EmailDraft.java b/experimental/fluent/agentic/src/test/java/org/acme/EmailDraft.java deleted file mode 100644 index c73824fd6..000000000 --- a/experimental/fluent/agentic/src/test/java/org/acme/EmailDraft.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 org.acme; - -import java.util.List; - -public record EmailDraft(String subject, String bodyPlain, List links) {} diff --git a/experimental/fluent/agentic/src/test/java/org/acme/EmailDrafts.java b/experimental/fluent/agentic/src/test/java/org/acme/EmailDrafts.java deleted file mode 100644 index c6ae51a4c..000000000 --- a/experimental/fluent/agentic/src/test/java/org/acme/EmailDrafts.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 org.acme; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public final class EmailDrafts { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - public static EmailDraft parse(String json) { - try { - var node = MAPPER.readTree(json); - var subject = node.path("subject").asText(""); - var bodyPlain = node.path("body_plain").asText(""); - var links = new ArrayList(); - node.path("links").forEach(n -> links.add(n.asText())); - return new EmailDraft(subject, bodyPlain, links); - } catch (IOException e) { - return new EmailDraft("", "", List.of()); - } - } -} diff --git a/experimental/fluent/agentic/src/test/java/org/acme/EmailPolicies.java b/experimental/fluent/agentic/src/test/java/org/acme/EmailPolicies.java deleted file mode 100644 index 8f2d72ab2..000000000 --- a/experimental/fluent/agentic/src/test/java/org/acme/EmailPolicies.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 org.acme; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - -public final class EmailPolicies { - - public static PolicyDecision policyCheck(EmailDraft d) { - var notes = new ArrayList(); - var subject = safeTrim(d.subject()); - var body = safeTrim(d.bodyPlain()); - - if (subject.isEmpty()) notes.add("Missing subject"); - if (body.isEmpty()) notes.add("Missing body"); - - var secret = Pattern.compile("(?i)(api[-_]?key|secret|password|token)\\s*[:=]\\s*\\S+"); - if (secret.matcher(body).find() || secret.matcher(subject).find()) { - notes.add("Suspected secret detected"); - return new PolicyDecision(Decision.BLOCKED, d, notes); - } - - var allow = Set.of("example.com", "acme.com"); - var badLinks = new ArrayList(); - for (String url : d.links() == null ? List.of() : d.links()) { - try { - String host = URI.create(url).getHost(); - if (host == null || allow.stream().noneMatch(host::endsWith)) { - badLinks.add(url); - } - } catch (IllegalArgumentException ignored) { - badLinks.add(url); - } - } - if (!badLinks.isEmpty()) { - notes.add("Non-allowed or malformed links: " + badLinks); - } - - var decision = notes.isEmpty() ? Decision.AUTO_SEND : Decision.REVIEW; - return new PolicyDecision(decision, new EmailDraft(subject, body, d.links()), notes); - } - - private static String safeTrim(String s) { - return s == null ? "" : s.trim(); - } - - public enum Decision { - AUTO_SEND, - REVIEW, - BLOCKED - } -} diff --git a/experimental/fluent/agentic/src/test/java/org/acme/PolicyDecision.java b/experimental/fluent/agentic/src/test/java/org/acme/PolicyDecision.java deleted file mode 100644 index 2acfd4358..000000000 --- a/experimental/fluent/agentic/src/test/java/org/acme/PolicyDecision.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 org.acme; - -import java.util.List; - -public record PolicyDecision( - EmailPolicies.Decision decision, EmailDraft draft, List notes) {} diff --git a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java index 8db4e1369..02d141a8d 100644 --- a/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java +++ b/experimental/fluent/func/src/main/java/io/serverlessworkflow/fluent/func/FuncForkTaskBuilder.java @@ -43,6 +43,7 @@ public class FuncForkTaskBuilder extends TaskBaseBuilder this.forkTask = new ForkTask(); this.forkTask.setFork(new ForkTaskConfiguration()); this.items = new ArrayList<>(); + this.setTask(forkTask); } @Override diff --git a/experimental/fluent/pom.xml b/experimental/fluent/pom.xml index 71969d9e5..2fa525b93 100644 --- a/experimental/fluent/pom.xml +++ b/experimental/fluent/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 io.serverlessworkflow @@ -48,11 +49,6 @@ serverlessworkflow-experimental-fluent-agentic ${project.version} - - io.serverlessworkflow - serverlessworkflow-experimental-fluent-agentic-langchain4j - ${project.version} - io.serverlessworkflow serverlessworkflow-experimental-fluent-agentic @@ -65,7 +61,5 @@ func - agentic - agentic-langchain4j \ No newline at end of file diff --git a/experimental/pom.xml b/experimental/pom.xml index 520ecb954..019a15b87 100644 --- a/experimental/pom.xml +++ b/experimental/pom.xml @@ -46,7 +46,6 @@ types lambda - agentic lambda-fluent fluent model diff --git a/pom.xml b/pom.xml index 827f49959..5c8e86959 100644 --- a/pom.xml +++ b/pom.xml @@ -92,10 +92,6 @@ 2.0.17 9.1.0.Final 6.0.0 - - 1.8.0-beta15 - - 1.8.0 2.1.36 @@ -257,11 +253,6 @@ ${version.awaitility} test - - dev.langchain4j - langchain4j-agentic - ${version.dev.langchain4j.beta} -