Skip to content

Introduce LangChain4j Agentic Workflow Implementation #681

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 8 additions & 7 deletions .github/workflows/maven-verify.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -14,22 +11,26 @@ 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:
distribution: temurin
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,17 +35,20 @@ public void setObject(Object obj) {

@Override
public Collection<WorkflowModel> asCollection() {
return object instanceof Collection value
? new AgenticModelCollection(value, cognisphere)
: Collections.emptyList();
throw new UnsupportedOperationException("Not supported yet.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ill fix this later after you merge

}

@Override
public Optional<Map<String, Object>> asMap() {
return Optional.of(((AgenticScope) object).state());
}

@Override
public <T> Optional<T> as(Class<T> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,37 @@
*/
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;
import java.util.Optional;

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 <T> Optional<T> as(Class<T> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, WorkflowModel> 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<String, Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class JavaModel implements WorkflowModel {

protected Object object;

protected JavaModel(Object object) {
public JavaModel(Object object) {
this.object = asJavaObject(object);
}

Expand Down
Loading