diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 00000000..37cac254 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,40 @@ +name: sdk-java Integration Tests +on: + issue_comment: + types: [ created ] + +jobs: + run-integration-tests: + # 2) Only run if the comment is exactly "/run integration-tests" + # and it’s on a pull request (not on an issue) + if: > + github.event.comment.body == '/run integration-tests' && + github.event.issue.pull_request != null + runs-on: ubuntu-latest + + permissions: + contents: read + pull-requests: write + checks: write + id-token: write + + steps: + # 3) Checkout the **PR’s** code + - name: Checkout PR code + uses: actions/checkout@v4 + with: + repository: ${{ github.event.issue.pull_request.head.repo.full_name }} + ref: ${{ github.event.issue.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + + # 4) Set up Java/Maven (cache enabled) + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: maven + + # 5) Run only the IT suite + - name: Run integration-tests profile + run: mvn -B -f pom.xml clean verify -Pintegration-tests \ No newline at end of file diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 2dd7539d..74e3dd69 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -1,6 +1,3 @@ -# This workflow will build a Java project with Maven -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven - name: sdk-java Verify on: @@ -14,9 +11,13 @@ on: jobs: build: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 + # 1. Checkout this repo + - name: Checkout sdk-java + uses: actions/checkout@v4 + # 2. Set up JDK 17 for both builds - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -24,12 +25,12 @@ jobs: java-version: 17 cache: 'maven' + # 3. Verify the main sdk-java project, excluding the two agentic modules - name: Verify with Maven run: | - mvn -B -f pom.xml clean install verify \ - -pl ",!fluent/agentic" -pl ",!experimental/agentic" \ - -am + mvn -B -f pom.xml clean install verify -am + # 4. Verify examples - name: Verify Examples with Maven run: | mvn -B -f examples/pom.xml clean install verify diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java index fb119ba1..3d352812 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModel.java @@ -15,21 +15,17 @@ */ package io.serverlessworkflow.impl.expressions.agentic; -import dev.langchain4j.agentic.cognisphere.Cognisphere; -import dev.langchain4j.agentic.cognisphere.ResultWithCognisphere; +import dev.langchain4j.agentic.scope.AgenticScope; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.expressions.func.JavaModel; import java.util.Collection; -import java.util.Collections; +import java.util.Map; import java.util.Optional; class AgenticModel extends JavaModel { - private final Cognisphere cognisphere; - - AgenticModel(Object object, Cognisphere cognisphere) { - super(object); - this.cognisphere = cognisphere; + AgenticModel(AgenticScope agenticScope) { + super(agenticScope); } @Override @@ -39,17 +35,20 @@ public void setObject(Object obj) { @Override public Collection asCollection() { - return object instanceof Collection value - ? new AgenticModelCollection(value, cognisphere) - : Collections.emptyList(); + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Optional> asMap() { + return Optional.of(((AgenticScope) object).state()); } @Override public Optional as(Class clazz) { - if (Cognisphere.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(cognisphere)); - } else if (ResultWithCognisphere.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(new ResultWithCognisphere<>(cognisphere, object))); + if (AgenticScope.class.isAssignableFrom(clazz)) { + return Optional.of(clazz.cast(object)); + } else if (Map.class.isAssignableFrom(clazz)) { + return Optional.of(clazz.cast(((AgenticScope) object).state())); } else { return super.as(clazz); } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java index e9440fb5..2ea0e382 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelCollection.java @@ -15,8 +15,8 @@ */ package io.serverlessworkflow.impl.expressions.agentic; -import dev.langchain4j.agentic.cognisphere.Cognisphere; -import dev.langchain4j.agentic.cognisphere.ResultWithCognisphere; +import dev.langchain4j.agentic.scope.AgenticScope; +import dev.langchain4j.agentic.scope.ResultWithAgenticScope; import io.serverlessworkflow.impl.WorkflowModel; import io.serverlessworkflow.impl.expressions.func.JavaModelCollection; import java.util.Collection; @@ -24,28 +24,28 @@ class AgenticModelCollection extends JavaModelCollection { - private final Cognisphere cognisphere; + private final AgenticScope agenticScope; - AgenticModelCollection(Collection object, Cognisphere cognisphere) { + AgenticModelCollection(Collection object, AgenticScope agenticScope) { super(object); - this.cognisphere = cognisphere; + this.agenticScope = agenticScope; } - AgenticModelCollection(Cognisphere cognisphere) { - this.cognisphere = cognisphere; + AgenticModelCollection(AgenticScope agenticScope) { + this.agenticScope = agenticScope; } @Override protected WorkflowModel nextItem(Object obj) { - return new AgenticModel(obj, cognisphere); + return new AgenticModel((AgenticScope) obj); } @Override public Optional as(Class clazz) { - if (Cognisphere.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(cognisphere)); - } else if (ResultWithCognisphere.class.isAssignableFrom(clazz)) { - return Optional.of(clazz.cast(new ResultWithCognisphere<>(cognisphere, object))); + 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 { return super.as(clazz); } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java index fe57b99e..9f95c0d5 100644 --- a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/AgenticModelFactory.java @@ -15,87 +15,96 @@ */ package io.serverlessworkflow.impl.expressions.agentic; -import dev.langchain4j.agentic.cognisphere.Cognisphere; -import dev.langchain4j.agentic.cognisphere.CognisphereRegistry; +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.expressions.agentic.langchain4j.AgenticScopeRegistryAssessor; +import io.serverlessworkflow.impl.expressions.func.JavaModel; import java.time.OffsetDateTime; import java.util.Map; class AgenticModelFactory implements WorkflowModelFactory { - private Cognisphere cognisphere = CognisphereRegistry.createEphemeralCognisphere(); - - private final AgenticModel TrueModel = new AgenticModel(Boolean.TRUE, cognisphere); - private final AgenticModel FalseModel = new AgenticModel(Boolean.FALSE, cognisphere); - private final AgenticModel NullModel = new AgenticModel(null, cognisphere); - - public void setCognishere(Cognisphere cognisphere) { - this.cognisphere = cognisphere; - } - + /** + * Applies any change to the model after running as task. We will always set it to a @AgenticScope + * object since @AgentExecutor is always adding the output to the agenticScope. We just have to + * make sure that agenticScope is always passed to the next input task. + * + * @param prev the global AgenticScope object getting updated by the workflow context + * @param obj the same AgenticScope object updated by the AgentExecutor + * @return the workflow context model holding the agenticScope object. + */ @Override public WorkflowModel fromAny(WorkflowModel prev, Object obj) { - ((AgenticModel) prev).setObject(obj); + // We ignore `obj` since it's already included in `prev` within the agenticScope instance return prev; } @Override public WorkflowModel combine(Map workflowVariables) { - return new AgenticModel(workflowVariables, cognisphere); + // TODO: create a new agenticScope object in the AgenticScopeRegistryAssessor per branch + // TODO: Since we share the same agenticScope object, both branches are updating the same + // instance, so for now we return the first key. + return workflowVariables.values().iterator().next(); } @Override public WorkflowModelCollection createCollection() { - return new AgenticModelCollection(cognisphere); + throw new UnsupportedOperationException(); } + // TODO: all these methods can use agenticScope as long as we have access to the `outputName` + @Override public WorkflowModel from(boolean value) { - return value ? TrueModel : FalseModel; + return new JavaModel(value); } @Override public WorkflowModel from(Number value) { - return new AgenticModel(value, cognisphere); + return new JavaModel(value); } @Override public WorkflowModel from(String value) { - return new AgenticModel(value, cognisphere); + return new JavaModel(value); } @Override public WorkflowModel from(CloudEvent ce) { - return new AgenticModel(ce, cognisphere); + return new JavaModel(ce); } @Override public WorkflowModel from(CloudEventData ce) { - return new AgenticModel(ce, cognisphere); + return new JavaModel(ce); } @Override public WorkflowModel from(OffsetDateTime value) { - return new AgenticModel(value, cognisphere); + return new JavaModel(value); } @Override public WorkflowModel from(Map map) { - cognisphere.writeStates(map); - return new AgenticModel(map, cognisphere); + final AgenticScope agenticScope = new AgenticScopeRegistryAssessor().getAgenticScope(); + agenticScope.writeStates(map); + return new AgenticModel(agenticScope); } @Override public WorkflowModel fromNull() { - return NullModel; + return new JavaModel(null); } @Override public WorkflowModel fromOther(Object value) { - return new AgenticModel(value, cognisphere); + if (value instanceof AgenticScope) { + return new AgenticModel((AgenticScope) value); + } + return new JavaModel(value); } } diff --git a/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java new file mode 100644 index 00000000..01ccd1cd --- /dev/null +++ b/experimental/agentic/src/main/java/io/serverlessworkflow/impl/expressions/agentic/langchain4j/AgenticScopeRegistryAssessor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl.expressions.agentic.langchain4j; + +import dev.langchain4j.agentic.internal.AgenticScopeOwner; +import dev.langchain4j.agentic.scope.AgenticScopeRegistry; +import dev.langchain4j.agentic.scope.DefaultAgenticScope; +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 DefaultAgenticScope 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 DefaultAgenticScope getAgenticScope() { + if (agenticScope != null) { + return agenticScope; + } + + if (memoryId != null) { + this.agenticScope = registry().getOrCreate(memoryId); + } else { + this.agenticScope = registry().createEphemeralAgenticScope(); + } + return this.agenticScope; + } + + @Override + public AgenticScopeOwner withAgenticScope(DefaultAgenticScope agenticScope) { + this.agenticScope = agenticScope; + return this; + } + + @Override + public AgenticScopeRegistry registry() { + agenticScopeRegistry.compareAndSet(null, new AgenticScopeRegistry(agentId)); + return agenticScopeRegistry.get(); + } +} diff --git a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java index 884d0fe4..897b8d5c 100644 --- a/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java +++ b/experimental/lambda/src/main/java/io/serverlessworkflow/impl/expressions/func/JavaModel.java @@ -28,7 +28,7 @@ public class JavaModel implements WorkflowModel { protected Object object; - protected JavaModel(Object object) { + public JavaModel(Object object) { this.object = asJavaObject(object); } diff --git a/fluent/agentic-langchain4j/pom.xml b/fluent/agentic-langchain4j/pom.xml new file mode 100644 index 00000000..3aa6ac92 --- /dev/null +++ b/fluent/agentic-langchain4j/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-fluent + 8.0.0-SNAPSHOT + + + serverlessworkflow-fluent-agentic-langchain4j + Serverless Workflow :: Fluent :: Agentic LangChain4j + Agentic Workflow DSL Implementation for langchain4j-agentic + + + + io.serverlessworkflow + serverlessworkflow-experimental-agentic + + + io.serverlessworkflow + serverlessworkflow-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-fluent-agentic + test-jar + test + + + + + + + + \ No newline at end of file diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java new file mode 100644 index 00000000..c7ed7d83 --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/AbstractAgentService.java @@ -0,0 +1,92 @@ +/* + * 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 outputName(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) { + return (S) this; + } + + @Override + public Workflow getDefinition() { + return this.workflowBuilder.build(); + } +} diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java new file mode 100644 index 00000000..2b3b0e2c --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ConditionalAgentServiceImpl.java @@ -0,0 +1,67 @@ +/* + * 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.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.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 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/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java new file mode 100644 index 00000000..7d5428be --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LC4JWorkflowBuilder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.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/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java new file mode 100644 index 00000000..d7bc5b17 --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/LoopAgentServiceImpl.java @@ -0,0 +1,63 @@ +/* + * 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.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.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 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; + } +} diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java new file mode 100644 index 00000000..54c1247d --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/ParallelAgentServiceImpl.java @@ -0,0 +1,50 @@ +/* + * 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.internal.AgentExecutor; +import dev.langchain4j.agentic.workflow.ParallelAgentService; +import java.util.List; +import java.util.concurrent.ExecutorService; + +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 executorService(ExecutorService executorService) { + this.workflowExecBuilder.withExecutorFactory(() -> executorService); + return this; + } + + @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()); + } +} diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java new file mode 100644 index 00000000..1fc25707 --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImpl.java @@ -0,0 +1,45 @@ +/* + * 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.internal.AgentExecutor; +import dev.langchain4j.agentic.workflow.SequentialAgentService; +import java.util.List; + +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; + } +} diff --git a/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java new file mode 100644 index 00000000..cf3315a9 --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowDefinitionBuilder.java @@ -0,0 +1,25 @@ +/* + * 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/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java new file mode 100644 index 00000000..0f7ce656 --- /dev/null +++ b/fluent/agentic-langchain4j/src/main/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowInvocationHandler.java @@ -0,0 +1,178 @@ +/* + * 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.expressions.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 outputName() { + 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(); + // outputName + if (method.getDeclaringClass() == AgentSpecification.class) { + return switch (method.getName()) { + case "outputName" -> outputName(); + default -> + throw new UnsupportedOperationException( + "Unknown method on AgentInstance class : " + method.getName()); + }; + } + // withCognisphere + 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 CognisphereOwner class : " + method.getName()); + }; + } + // getAgenticScope + // evictCognisphere + 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(currentCognisphere(method, args), method, args); + } + + private Object executeWorkflow(DefaultAgenticScope agenticScope, Method method, Object[] args) { + writeAgenticScopeState(agenticScope, method, args); + + try (WorkflowApplication app = workflowApplicationBuilder.build()) { + // TODO improve result handling + DefaultAgenticScope output = + app.workflowDefinition(workflow) + .instance(agenticScope) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow( + () -> + new IllegalArgumentException( + "Workflow hasn't returned a Cognisphere object.")); + Object result = output.readState(outputName()); + + 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 DefaultAgenticScope currentCognisphere(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/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java new file mode 100644 index 00000000..d29a01af --- /dev/null +++ b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Agents.java @@ -0,0 +1,253 @@ +/* + * 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.agent.tool.Tool; +import dev.langchain4j.agentic.Agent; +import dev.langchain4j.agentic.scope.AgenticScopeAccess; +import dev.langchain4j.agentic.scope.ResultWithAgenticScope; +import dev.langchain4j.service.MemoryId; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; +import java.util.List; + +public class Agents { + + public interface ExpertRouterAgent { + + @Agent + String ask(@V("request") String request); + } + + public interface ExpertRouterAgentWithMemory extends AgenticScopeAccess { + + @Agent + String ask(@MemoryId String memoryId, @V("request") String request); + } + + public 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("Categorize a user request") + RequestCategory classify(@V("request") String request); + } + + public enum RequestCategory { + LEGAL, + MEDICAL, + TECHNICAL, + UNKNOWN + } + + public interface RouterAgent { + + @UserMessage( + """ + Analyze the following user request and categorize it as 'legal', 'medical' or 'technical', + then forward the request as it is to the corresponding expert provided as a tool. + Finally return the answer that you received from the expert without any modification. + + The user request is: '{{it}}'. + """) + @Agent + String askToExpert(String request); + } + + public interface MedicalExpert { + + @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 {{request}}. + """) + @Tool("A medical expert") + @Agent("A medical expert") + String medical(@V("request") String request); + } + + public interface MedicalExpertWithMemory { + + @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 {{request}}. + """) + @Tool("A medical expert") + @Agent("A medical expert") + String medical(@MemoryId String memoryId, @V("request") String request); + } + + public interface LegalExpert { + + @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 {{request}}. + """) + @Tool("A legal expert") + @Agent("A legal expert") + String legal(@V("request") String request); + } + + public interface LegalExpertWithMemory { + + @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 {{request}}. + """) + @Tool("A legal expert") + @Agent("A legal expert") + String legal(@MemoryId String memoryId, @V("request") String request); + } + + public interface TechnicalExpert { + + @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 {{request}}. + """) + @Tool("A technical expert") + @Agent("A technical expert") + String technical(@V("request") String request); + } + + public interface TechnicalExpertWithMemory { + + @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 {{request}}. + """) + @Tool("A technical expert") + @Agent("A technical expert") + String technical(@MemoryId String memoryId, @V("request") String request); + } + + 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); + } + + public 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("Score a story based on how well it aligns with a given style") + double scoreStyle(@V("story") String story, @V("style") String style); + } + + public interface StyleReviewLoop { + + @Agent("Review the given story to ensure it aligns with the specified style") + String scoreAndReview(@V("story") String story, @V("style") String style); + } + + public interface StyledWriter extends AgenticScopeAccess { + + @Agent + ResultWithAgenticScope writeStoryWithStyle( + @V("topic") String topic, @V("style") String style); + } + + 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); + } + + public record EveningPlan(String movie, String meal) {} + + public interface EveningPlannerAgent { + + @Agent + List plan(@V("mood") String mood); + } +} diff --git a/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java new file mode 100644 index 00000000..c3be6250 --- /dev/null +++ b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/Models.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic.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/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java new file mode 100644 index 00000000..1902f2ce --- /dev/null +++ b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/SequentialAgentServiceImplTest.java @@ -0,0 +1,119 @@ +/* + * 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).outputName(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/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java new file mode 100644 index 00000000..52e4d86f --- /dev/null +++ b/fluent/agentic-langchain4j/src/test/java/io/serverlessworkflow/fluent/agentic/langchain4j/WorkflowAgentsIT.java @@ -0,0 +1,80 @@ +/* + * 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) + .outputName("story") + .build()); + + AudienceEditor audienceEditor = + spy( + AgenticServices.agentBuilder(AudienceEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build()); + + StyleEditor styleEditor = + spy( + AgenticServices.agentBuilder(StyleEditor.class) + .chatModel(BASE_MODEL) + .outputName("story") + .build()); + + UntypedAgent novelCreator = + builder + .sequenceBuilder() + .subAgents(creativeWriter, audienceEditor, styleEditor) + .outputName("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/fluent/agentic/pom.xml b/fluent/agentic/pom.xml index 849bf747..8946e189 100644 --- a/fluent/agentic/pom.xml +++ b/fluent/agentic/pom.xml @@ -12,12 +12,6 @@ Serverless Workflow :: Fluent :: Agentic serverlessworkflow-fluent-agentic - - 17 - 17 - UTF-8 - - io.serverlessworkflow @@ -31,12 +25,12 @@ dev.langchain4j langchain4j-agentic + org.slf4j slf4j-simple test - org.junit.jupiter junit-jupiter-api @@ -50,19 +44,36 @@ org.assertj assertj-core + test dev.langchain4j langchain4j-ollama test - 1.2.0-SNAPSHOT + ${version.dev.langchain4j} io.serverlessworkflow serverlessworkflow-experimental-agentic - ${project.version} test + + + + maven-jar-plugin + ${version.jar.plugin} + + + + + test-jar + + + + + + + \ No newline at end of file diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java index ebcde632..8127174d 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java @@ -17,9 +17,10 @@ import static dev.langchain4j.agentic.internal.AgentUtil.agentsToExecutors; -import dev.langchain4j.agentic.cognisphere.Cognisphere; import dev.langchain4j.agentic.internal.AgentExecutor; -import dev.langchain4j.agentic.internal.AgentInstance; +import dev.langchain4j.agentic.internal.AgentSpecification; +import dev.langchain4j.agentic.scope.AgenticScope; +import dev.langchain4j.agentic.scope.DefaultAgenticScope; import io.serverlessworkflow.impl.expressions.LoopPredicateIndex; import java.util.List; import java.util.function.Function; @@ -31,14 +32,14 @@ public final class AgentAdapters { private AgentAdapters() {} public static List toExecutors(Object... agents) { - return agentsToExecutors(Stream.of(agents).map(AgentInstance.class::cast).toArray()); + return agentsToExecutors(Stream.of(agents).map(AgentSpecification.class::cast).toArray()); } - public static Function toFunction(AgentExecutor exec) { - return exec::invoke; + public static Function toFunction(AgentExecutor exec) { + return exec::execute; } - public static LoopPredicateIndex toWhile(Predicate exit) { + public static LoopPredicateIndex toWhile(Predicate exit) { return (model, item, idx) -> !exit.test(model); } } diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java index a69a4bd1..526deac0 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java @@ -57,6 +57,12 @@ public AgentDoTaskBuilder loop(String name, Consumer 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); diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java index 60329e43..a26f0b45 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -15,8 +15,8 @@ */ package io.serverlessworkflow.fluent.agentic; -import dev.langchain4j.agentic.cognisphere.Cognisphere; 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; @@ -57,7 +57,8 @@ public AgentTaskItemListBuilder agent(String name, Object agent) { .forEach( exec -> this.delegate.callFn( - name, fn -> fn.function(AgentAdapters.toFunction(exec), Cognisphere.class))); + name, + fn -> fn.function(AgentAdapters.toFunction(exec), DefaultAgenticScope.class))); return self(); } @@ -73,6 +74,12 @@ public AgentTaskItemListBuilder sequence(String name, Object... agents) { 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(); } @@ -86,7 +93,9 @@ public AgentTaskItemListBuilder parallel(String name, Object... agents) { for (int i = 0; i < execs.size(); i++) { AgentExecutor ex = execs.get(i); fork.branch( - "branch-" + i + "-" + name, AgentAdapters.toFunction(ex), Cognisphere.class); + "branch-" + i + "-" + name, + AgentAdapters.toFunction(ex), + DefaultAgenticScope.class); } }); return self(); diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java index bd943ebc..e0eeb0c6 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java @@ -15,12 +15,13 @@ */ package io.serverlessworkflow.fluent.agentic; +import io.serverlessworkflow.fluent.func.spi.FuncTransformations; import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; import java.util.UUID; public class AgentWorkflowBuilder - extends BaseWorkflowBuilder< - AgentWorkflowBuilder, AgentDoTaskBuilder, AgentTaskItemListBuilder> { + extends BaseWorkflowBuilder + implements FuncTransformations { AgentWorkflowBuilder(final String name, final String namespace, final String version) { super(name, namespace, version); diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java index f98089c8..01b71015 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java @@ -15,8 +15,8 @@ */ package io.serverlessworkflow.fluent.agentic; -import dev.langchain4j.agentic.cognisphere.Cognisphere; 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; @@ -31,7 +31,7 @@ public class LoopAgentsBuilder { private final FuncTaskItemListBuilder funcDelegate; private final ForTaskFunction forTask; - LoopAgentsBuilder() { + public LoopAgentsBuilder() { this.forTask = new ForTaskFunction(); this.forTask.setFor(new ForTaskConfiguration()); this.funcDelegate = new FuncTaskItemListBuilder(); @@ -60,8 +60,8 @@ public LoopAgentsBuilder maxIterations(int maxIterations) { return this; } - public LoopAgentsBuilder exitCondition(Predicate exitCondition) { - this.forTask.withWhile(AgentAdapters.toWhile(exitCondition), Cognisphere.class); + public LoopAgentsBuilder exitCondition(Predicate exitCondition) { + this.forTask.withWhile(AgentAdapters.toWhile(exitCondition), AgenticScope.class); return this; } diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java index c23f8ef3..aaa7176f 100644 --- a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java @@ -40,6 +40,12 @@ 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) { diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java index 008fe48b..63477fba 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java @@ -24,8 +24,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.spy; -import dev.langchain4j.agentic.AgentServices; -import dev.langchain4j.agentic.cognisphere.Cognisphere; +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; @@ -43,7 +43,7 @@ class AgentWorkflowBuilderTest { public void verifyAgentCall() { Agents.MovieExpert movieExpert = spy( - AgentServices.agentBuilder(Agents.MovieExpert.class) + AgenticServices.agentBuilder(Agents.MovieExpert.class) .outputName("movies") .chatModel(BASE_MODEL) .build()); @@ -117,7 +117,7 @@ void loopWithMaxIterationsAndExitCondition() { Agents.MovieExpert expert = newMovieExpert(); AtomicInteger max = new AtomicInteger(4); - Predicate exit = + Predicate exit = cog -> { // stop when we already have at least one movie picked in state var movies = cog.readState("movies", null); @@ -189,7 +189,7 @@ void testWorkflowCallFnBare() { @Test @DisplayName("workflow callFn with Java DSL guard attaches predicate") void testWorkflowCallFnWithPredicate() { - Predicate guard = cog -> true; + Predicate guard = cog -> true; Workflow wf = AgentWorkflowBuilder.workflow() diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java index 7215ddd6..a0c970ab 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -16,7 +16,7 @@ package io.serverlessworkflow.fluent.agentic; import dev.langchain4j.agentic.Agent; -import dev.langchain4j.agentic.internal.AgentInstance; +import dev.langchain4j.agentic.internal.AgentSpecification; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; import java.util.List; @@ -36,7 +36,7 @@ interface MovieExpert { List findMovie(@V("mood") String mood); } - interface SettingAgent extends AgentInstance { + interface SettingAgent extends AgentSpecification { @UserMessage( """ @@ -49,7 +49,7 @@ interface SettingAgent extends AgentInstance { String invoke(@V("style") String style); } - interface HeroAgent extends AgentInstance { + interface HeroAgent extends AgentSpecification { @UserMessage( """ @@ -60,7 +60,7 @@ interface HeroAgent extends AgentInstance { String invoke(@V("style") String style); } - interface ConflictAgent extends AgentInstance { + interface ConflictAgent extends AgentSpecification { @UserMessage( """ @@ -72,7 +72,7 @@ interface ConflictAgent extends AgentInstance { String invoke(@V("style") String style); } - interface FactAgent extends AgentInstance { + interface FactAgent extends AgentSpecification { @UserMessage( """ @@ -82,7 +82,7 @@ interface FactAgent extends AgentInstance { String invoke(@V("fact") String fact); } - interface CultureAgent extends AgentInstance { + interface CultureAgent extends AgentSpecification { @UserMessage( """ @@ -94,7 +94,7 @@ interface CultureAgent extends AgentInstance { List invoke(@V("fact") String fact); } - interface TechnologyAgent extends AgentInstance { + interface TechnologyAgent extends AgentSpecification { @UserMessage( """ @@ -106,7 +106,7 @@ interface TechnologyAgent extends AgentInstance { List invoke(@V("fact") String fact); } - interface StorySeedAgent extends AgentInstance { + interface StorySeedAgent extends AgentSpecification { @UserMessage( """ @@ -117,7 +117,7 @@ You are a science fiction writer. Given the following title, come up with a shor String invoke(@V("title") String title); } - interface PlotAgent extends AgentInstance { + interface PlotAgent extends AgentSpecification { @UserMessage( """ @@ -129,7 +129,7 @@ interface PlotAgent extends AgentInstance { String invoke(@V("premise") String premise); } - interface SceneAgent extends AgentInstance { + interface SceneAgent extends AgentSpecification { @UserMessage( """ @@ -141,7 +141,7 @@ interface SceneAgent extends AgentInstance { String invoke(@V("plot") String plot); } - interface MeetingInvitationDraft extends AgentInstance { + interface MeetingInvitationDraft extends AgentSpecification { @UserMessage( """ @@ -161,7 +161,7 @@ String invoke( @V("agenda") String agenda); } - interface MeetingInvitationStyle extends AgentInstance { + interface MeetingInvitationStyle extends AgentSpecification { @UserMessage( """ diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java index a59f62e1..f44d2a22 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -18,7 +18,7 @@ import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; import static org.mockito.Mockito.spy; -import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.AgenticServices; public final class AgentsUtils { @@ -26,7 +26,7 @@ private AgentsUtils() {} public static Agents.MovieExpert newMovieExpert() { return spy( - AgentServices.agentBuilder(Agents.MovieExpert.class) + AgenticServices.agentBuilder(Agents.MovieExpert.class) .outputName("movies") .chatModel(BASE_MODEL) .build()); diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java index 8bbbe62d..acf5c411 100644 --- a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/WorkflowTests.java @@ -18,23 +18,20 @@ import static io.serverlessworkflow.fluent.agentic.Agents.*; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.AgenticServices; +import dev.langchain4j.agentic.scope.DefaultAgenticScope; 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.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; -import java.util.stream.Collectors; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -56,10 +53,15 @@ public void testAgent() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - String result = - app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + DefaultAgenticScope result = + app.workflowDefinition(workflow) + .instance(topic) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow(); - assertEquals("storySeedAgent", result); + assertEquals("storySeedAgent", result.readState("premise")); } } @@ -91,10 +93,15 @@ public void testAgents() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - String result = - app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + DefaultAgenticScope result = + app.workflowDefinition(workflow) + .instance(topic) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow(); - assertEquals("sceneAgent", result); + assertEquals("sceneAgent", result.readState("story")); } } @@ -122,10 +129,15 @@ public void testSequence() throws ExecutionException, InterruptedException { topic.put("title", "A Great Story"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - String result = - app.workflowDefinition(workflow).instance(topic).start().get().asText().orElseThrow(); + DefaultAgenticScope result = + app.workflowDefinition(workflow) + .instance(topic) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow(); - assertEquals("sceneAgent", result); + assertEquals("sceneAgent", result.readState("story")); } } @@ -154,25 +166,21 @@ public void testParallel() throws ExecutionException, InterruptedException { topic.put("style", "sci-fi"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - Map result = - app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); - - assertEquals(3, result.size()); - assertTrue(result.containsKey("branch-0-story")); - assertTrue(result.containsKey("branch-1-story")); - assertTrue(result.containsKey("branch-2-story")); - - Set values = - result.values().stream().map(Object::toString).collect(Collectors.toSet()); + DefaultAgenticScope result = + app.workflowDefinition(workflow) + .instance(topic) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow(); - assertTrue(values.contains("Fake conflict response")); - assertTrue(values.contains("Fake hero response")); - assertTrue(values.contains("Fake setting response")); + assertEquals("Fake conflict response", result.readState("setting")); + assertEquals("Fake hero response", result.readState("hero")); + assertEquals("Fake setting response", result.readState("conflict")); } } @Test - // TODO: callFn must be replace with a .output() method once it's available public void testSeqAndThenParallel() throws ExecutionException, InterruptedException { final FactAgent factAgent = mock(FactAgent.class); final CultureAgent cultureAgent = mock(CultureAgent.class); @@ -197,15 +205,6 @@ public void testSeqAndThenParallel() throws ExecutionException, InterruptedExcep .tasks( d -> d.sequence("fact", factAgent) - .callFn( - f -> - f.function( - (Function>) - fact -> { - Map result = new HashMap<>(); - result.put("fact", fact); - return result; - })) .parallel("cultureAndTechnology", cultureAgent, technologyAgent)) .build(); @@ -213,20 +212,22 @@ public void testSeqAndThenParallel() throws ExecutionException, InterruptedExcep topic.put("fact", "alien"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - Map result = - app.workflowDefinition(workflow).instance(topic).start().get().asMap().orElseThrow(); - - assertEquals(2, result.size()); - assertTrue(result.containsKey("branch-0-cultureAndTechnology")); - assertTrue(result.containsKey("branch-1-cultureAndTechnology")); + DefaultAgenticScope result = + app.workflowDefinition(workflow) + .instance(topic) + .start() + .get() + .as(DefaultAgenticScope.class) + .orElseThrow(); - assertEquals(cultureTraits, result.get("branch-0-cultureAndTechnology")); - assertEquals(technologyTraits, result.get("branch-1-cultureAndTechnology")); + assertEquals(cultureTraits, result.readState("culture")); + assertEquals(technologyTraits, result.readState("technology")); } } @Test - @Disabled("HumanLoop not implemented yet") + @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( @@ -246,7 +247,7 @@ public void humanInTheLoop() throws ExecutionException, InterruptedException { AtomicReference request = new AtomicReference<>(); HumanInTheLoop humanInTheLoop = - AgentServices.humanInTheLoopBuilder() + AgenticServices.humanInTheLoopBuilder() .description( "What level of formality would you like? (please reply with “formal”, “casual”, or “friendly”)") .inputName("style") @@ -273,15 +274,15 @@ public void humanInTheLoop() throws ExecutionException, InterruptedException { initialValues.put("agenda", "Discuss project updates"); try (WorkflowApplication app = WorkflowApplication.builder().build()) { - String result = + DefaultAgenticScope result = app.workflowDefinition(workflow) .instance(initialValues) .start() .get() - .asText() + .as(DefaultAgenticScope.class) .orElseThrow(); - assertEquals("Styled meeting invitation for John Doe", result); + assertEquals("Styled meeting invitation for John Doe", result.readState("styled")); } } } diff --git a/fluent/pom.xml b/fluent/pom.xml index 51aac994..508064ba 100644 --- a/fluent/pom.xml +++ b/fluent/pom.xml @@ -40,6 +40,33 @@ serverlessworkflow-fluent-func ${project.version} + + io.serverlessworkflow + serverlessworkflow-fluent-agentic + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-fluent-agentic-langchain4j + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-experimental-agentic + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-experimental-lambda + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-fluent-agentic + ${project.version} + test-jar + test + @@ -47,6 +74,7 @@ spec func agentic + agentic-langchain4j \ No newline at end of file diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java index 2ab0bb17..3aed00ad 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseWorkflowBuilder.java @@ -21,6 +21,7 @@ import io.serverlessworkflow.api.types.Output; import io.serverlessworkflow.api.types.Workflow; import io.serverlessworkflow.fluent.spec.spi.TransformationHandlers; +import java.util.UUID; import java.util.function.Consumer; public abstract class BaseWorkflowBuilder< @@ -42,6 +43,9 @@ protected BaseWorkflowBuilder(final String name, final String namespace, final S this.document.setNamespace(namespace); this.document.setVersion(version); this.document.setDsl(DSL); + if (this.document.getName() == null || this.document.getName().isEmpty()) { + this.document.setName(UUID.randomUUID().toString()); + } this.workflow = new Workflow(); this.workflow.setDocument(this.document); } diff --git a/pom.xml b/pom.xml index 471a0028..513cf126 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,11 @@ 2.0.17 9.0.1.Final 6.0.0 - 1.3.0-beta9-SNAPSHOT + + 1.3.0-beta9 + + 1.3.0 + true java true + + **/*IT.java @@ -226,7 +232,7 @@ dev.langchain4j langchain4j-agentic - ${version.dev.langchain4j} + ${version.dev.langchain4j.beta} @@ -442,6 +448,9 @@ ${version.surefire.plugin} -Xmx1024m -XX:+IgnoreUnrecognizedVMOptions -XX:MaxPermSize=256m + + ${integration-tests.includes} + @@ -450,6 +459,9 @@ ${version.failsafe.plugin} -Xmx1024m -XX:+IgnoreUnrecognizedVMOptions -XX:MaxPermSize=256m + + ${integration-tests.includes} + @@ -565,5 +577,28 @@ + + integration-tests + + false + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + run-integration-tests + + integration-test + verify + + + + + + +