diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 2070974b..9449b5a1 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -10,6 +10,7 @@ on: pull_request: branches: - main + jobs: build: runs-on: ubuntu-latest @@ -25,8 +26,10 @@ jobs: - name: Verify with Maven run: | - mvn -B -f pom.xml clean install verify + mvn -B -f pom.xml clean install verify \ + -pl ",!fluent/agentic" \ + -am - name: Verify Examples with Maven run: | - mvn -B -f examples/pom.xml clean install verify + mvn -B -f examples/pom.xml clean install verify \ No newline at end of file diff --git a/fluent/agentic/pom.xml b/fluent/agentic/pom.xml new file mode 100644 index 00000000..2ebce363 --- /dev/null +++ b/fluent/agentic/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-fluent + 8.0.0-SNAPSHOT + + + Serverless Workflow :: Fluent :: Agentic + serverlessworkflow-fluent-agentic + + + 17 + 17 + UTF-8 + + 1.2.0-beta8-SNAPSHOT + + + + + io.serverlessworkflow + serverlessworkflow-experimental-types + + + io.serverlessworkflow + serverlessworkflow-fluent-func + + + dev.langchain4j + langchain4j-agentic + ${version.dev.langchain4j} + + + org.slf4j + slf4j-simple + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + + + dev.langchain4j + langchain4j-ollama + test + 1.2.0-SNAPSHOT + + + + \ No newline at end of file diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java new file mode 100644 index 00000000..eafd2a82 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentAdapters.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import static dev.langchain4j.agentic.internal.AgentExecutor.agentsToExecutors; + +import dev.langchain4j.agentic.Cognisphere; +import dev.langchain4j.agentic.internal.AgentExecutor; +import dev.langchain4j.agentic.internal.AgentInstance; +import io.serverlessworkflow.impl.expressions.LoopPredicateIndex; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +public final class AgentAdapters { + private AgentAdapters() {} + + public static List toExecutors(Object... agents) { + return agentsToExecutors(Stream.of(agents).map(AgentInstance.class::cast).toList()); + } + + public static Function toFunction(AgentExecutor exec) { + return exec::invoke; + } + + public static LoopPredicateIndex toWhile(Predicate exit) { + return (model, item, idx) -> !exit.test((Cognisphere) model); + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java new file mode 100644 index 00000000..a69a4bd1 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentDoTaskBuilder.java @@ -0,0 +1,108 @@ +/* + * 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 parallel(String name, Object... agents) { + this.listBuilder().parallel(name, agents); + return self(); + } + + @Override + public AgentDoTaskBuilder callFn(String name, Consumer cfg) { + this.listBuilder().callFn(name, cfg); + return self(); + } + + @Override + public AgentDoTaskBuilder emit(String name, Consumer itemsConfigurer) { + this.listBuilder().emit(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/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java new file mode 100644 index 00000000..5aed3127 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilder.java @@ -0,0 +1,134 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import dev.langchain4j.agentic.internal.AgentExecutor; +import io.serverlessworkflow.api.types.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.callFn(name, fn -> fn.function(AgentAdapters.toFunction(exec)))); + 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.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); + fork.branch("branch-" + i + "-" + name, AgentAdapters.toFunction(ex)); + } + }); + return self(); + } + + @Override + public AgentTaskItemListBuilder callFn(String name, Consumer cfg) { + this.delegate.callFn(name, cfg); + return self(); + } + + @Override + public AgentTaskItemListBuilder emit(String name, Consumer itemsConfigurer) { + this.delegate.emit(name, itemsConfigurer); + 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/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java new file mode 100644 index 00000000..bd943ebc --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import io.serverlessworkflow.fluent.spec.BaseWorkflowBuilder; +import java.util.UUID; + +public class AgentWorkflowBuilder + extends BaseWorkflowBuilder< + AgentWorkflowBuilder, AgentDoTaskBuilder, AgentTaskItemListBuilder> { + + AgentWorkflowBuilder(final String name, final String namespace, final String version) { + super(name, namespace, version); + } + + 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); + } + + @Override + protected AgentDoTaskBuilder newDo() { + return new AgentDoTaskBuilder(); + } + + @Override + protected AgentWorkflowBuilder self() { + return this; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java new file mode 100644 index 00000000..732b4c95 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/LoopAgentsBuilder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import dev.langchain4j.agentic.Cognisphere; +import dev.langchain4j.agentic.internal.AgentExecutor; +import io.serverlessworkflow.api.types.ForTaskConfiguration; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import io.serverlessworkflow.fluent.func.FuncTaskItemListBuilder; +import java.util.List; +import java.util.UUID; +import java.util.function.ObjIntConsumer; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +public class LoopAgentsBuilder { + + private final FuncTaskItemListBuilder funcDelegate; + private final ForTaskFunction forTask; + + LoopAgentsBuilder() { + this.forTask = new ForTaskFunction(); + this.forTask.setFor(new ForTaskConfiguration()); + this.funcDelegate = new FuncTaskItemListBuilder(); + } + + private static void forEachIndexed(List list, ObjIntConsumer consumer) { + IntStream.range(0, list.size()).forEach(i -> consumer.accept(list.get(i), i)); + } + + public LoopAgentsBuilder subAgents(String baseName, Object... agents) { + List execs = AgentAdapters.toExecutors(agents); + forEachIndexed( + execs, + (exec, idx) -> + funcDelegate.callFn( + baseName + "-" + idx, fn -> fn.function(AgentAdapters.toFunction(exec)))); + return this; + } + + public LoopAgentsBuilder subAgents(Object... agents) { + return this.subAgents("agent-" + UUID.randomUUID(), agents); + } + + public LoopAgentsBuilder maxIterations(int maxIterations) { + this.forTask.withCollection(ignored -> IntStream.range(0, maxIterations).boxed().toList()); + return this; + } + + public LoopAgentsBuilder exitCondition(Predicate exitCondition) { + this.forTask.withWhile(AgentAdapters.toWhile(exitCondition)); + return this; + } + + public ForTaskFunction build() { + this.forTask.setDo(this.funcDelegate.build()); + return this.forTask; + } +} diff --git a/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java new file mode 100644 index 00000000..c23f8ef3 --- /dev/null +++ b/fluent/agentic/src/main/java/io/serverlessworkflow/fluent/agentic/spi/AgentDoFluent.java @@ -0,0 +1,48 @@ +/* + * 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.LoopAgentsBuilder; +import io.serverlessworkflow.fluent.func.spi.FuncDoFluent; +import java.util.UUID; +import java.util.function.Consumer; + +public interface AgentDoFluent> extends FuncDoFluent { + + 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 parallel(String name, Object... agents); + + default SELF parallel(Object... agents) { + return parallel("par-" + UUID.randomUUID(), agents); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java new file mode 100644 index 00000000..8b9585db --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentTaskItemListBuilderTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.func.CallTaskJava; +import io.serverlessworkflow.api.types.func.ForTaskFunction; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Structural tests for AgentTaskItemListBuilder. */ +class AgentTaskItemListBuilderTest { + + @Test + @DisplayName("agent(name,obj) adds a CallTaskJava-backed TaskItem") + void testAgentAddsCallTask() { + AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); + Agents.MovieExpert agent = AgentsUtils.newMovieExpert(); + + b.agent("my-agent", agent); + + List items = b.build(); + assertThat(items).hasSize(1); + TaskItem item = items.get(0); + assertThat(item.getName()).isEqualTo("my-agent"); + + Task task = item.getTask(); + assertThat(task.getCallTask()).isInstanceOf(CallTaskJava.class); + } + + @Test + @DisplayName("sequence(name, agents...) expands to N CallTask items, in order") + void testSequence() { + AgentTaskItemListBuilder b = new AgentTaskItemListBuilder(); + Agents.MovieExpert a1 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a2 = AgentsUtils.newMovieExpert(); + Agents.MovieExpert a3 = AgentsUtils.newMovieExpert(); + + b.sequence("seq", a1, a2, a3); + + List items = b.build(); + assertThat(items).hasSize(3); + assertThat(items.get(0).getName()).isEqualTo("seq-0"); + assertThat(items.get(1).getName()).isEqualTo("seq-1"); + assertThat(items.get(2).getName()).isEqualTo("seq-2"); + + // All must be call 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/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java new file mode 100644 index 00000000..e34652c5 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentWorkflowBuilderTest.java @@ -0,0 +1,173 @@ +/* + * 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.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgentServices; +import dev.langchain4j.agentic.Cognisphere; +import io.serverlessworkflow.api.types.ForkTask; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.api.types.Workflow; +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( + AgentServices.agentBuilder(Agents.MovieExpert.class) + .outputName("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); + }); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java new file mode 100644 index 00000000..8248c28c --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Agents.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import dev.langchain4j.agentic.Agent; +import dev.langchain4j.service.UserMessage; +import dev.langchain4j.service.V; +import java.util.List; + +public interface Agents { + + interface MovieExpert { + + @UserMessage( + """ + You are a great evening planner. + Propose a list of 3 movies matching the given mood. + The mood is {mood}. + Provide a list with the 3 items and nothing else. + """) + @Agent + List findMovie(@V("mood") String mood); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java new file mode 100644 index 00000000..a59f62e1 --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/AgentsUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import static io.serverlessworkflow.fluent.agentic.Models.BASE_MODEL; +import static org.mockito.Mockito.spy; + +import dev.langchain4j.agentic.AgentServices; + +public final class AgentsUtils { + + private AgentsUtils() {} + + public static Agents.MovieExpert newMovieExpert() { + return spy( + AgentServices.agentBuilder(Agents.MovieExpert.class) + .outputName("movies") + .chatModel(BASE_MODEL) + .build()); + } +} diff --git a/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java new file mode 100644 index 00000000..e06aafda --- /dev/null +++ b/fluent/agentic/src/test/java/io/serverlessworkflow/fluent/agentic/Models.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.agentic; + +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; +import java.time.Duration; + +public class Models { + static final ChatModel BASE_MODEL = + OllamaChatModel.builder() + .baseUrl("http://127.0.0.1:1143") + .modelName("qwen2.5:7b") + .timeout(Duration.ofMinutes(10)) + .temperature(0.0) + .logRequests(true) + .logResponses(true) + .build(); +} diff --git a/fluent/pom.xml b/fluent/pom.xml index 4d056c48..51aac994 100644 --- a/fluent/pom.xml +++ b/fluent/pom.xml @@ -46,6 +46,7 @@ spec func + agentic \ No newline at end of file