LangGraph for Java. A library for building stateful, multi-agents applications with LLMs, built for work with langchain4j
It is a porting of original LangGraph from LangChain AI project in Java fashion
- StateGraph
- Nodes
- Edges
- Conditional Edges
- Entry Points
- Conditional Entry Points
- State
- Schema (a series of Channels)
- Reducer (how apply updates to the state attributes)
- Default provider
- AppenderChannel (values accumulator)
- Schema (a series of Channels)
- Compiling graph
- Async support (throught CompletableFuture)
- Streaming support (throught java-async-generator)
- Checkpoints (save and replay feature)
- Graph visualization
- Playground (Embeddable Webapp that plays with LangGraph4j)
- Threads (checkpointing of multiple different runs)
- Update state (interact with the state directly and update it)
- Breakpoints (pause and resume feature)
- Studio (Playground Webapp)
- Parallel Node Execution
- Parallel Node Execution
- Child Graphs
- Streaming response from LLM results
Project | Integrated With |
---|---|
Agent Executor | SpringAI |
Agent Executor | Langchain4j |
Image To PlantUML Diagram | Langchain4j |
Adaptive RAG | Langchain4j |
Date | Release | info |
---|---|---|
Oct 12, 2024 | 1.0-rc2 |
official release candidate |
Maven
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core-jdk8</artifactId>
<version>1.0-rc2</version>
</dependency>
Maven
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core-jdk8</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
The main type of graph in langgraph
is the StatefulGraph
. This graph is parameterized by a state object that it passes around to each node.
Each node then returns operations to update that state. These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.
Whether to set or add is described in the state's schema provided to the graph. The schema is a Map of Channels, each Channel represent an attribute in the state. If an attribute is described with an AppendeChannel
it will be a List and each element referring the attribute will be automaically added by graph during processing. The State must inherit from AgentState
base class (that essentially is a Map
wrapper).
public class AgentState {
public AgentState( Map<String,Object> initData ) { ... }
public final java.util.Map<String,Object> data() { ... }
public final <T> Optional<T> value(String key) { ... }
public final <T> T value(String key, T defaultValue ) { ... }
public final <T> T value(String key, Supplier<T> defaultProvider ) { ... }
}
We now need to define a few different nodes in our graph. In langgraph
, a node is an async/sync function that accept an AgentState
as argument and returns a (partial) state update. There are two main nodes we need for this:
- The agent: responsible for deciding what (if any) actions to take.
- A function to invoke tools: if the agent decides to take an action, this node will then execute that action.
/**
* Represents an asynchronous node action that operates on an agent state and returns state update.
*
* @param <S> the type of the agent state
*/
@FunctionalInterface
public interface AsyncNodeAction<S extends AgentState> extends Function<S, CompletableFuture<Map<String, Object>>> {
CompletableFuture<Map<String, Object>> apply(S t);
/**
* Creates an asynchronous node action from a synchronous node action.
*/
static <S extends AgentState> AsyncNodeAction<S> node_async(NodeAction<S> syncAction) { ... }
}
We will also need to define some edges. Some of these edges may be conditional. The reason they are conditional is that based on the output of a node, one of several paths may be taken. The path that is taken is not known until that node is run (the LLM decides).
- Conditional Edge: after the agent is called, we should either:
- If the agent said to take an action, then the function to invoke tools should be called
- If the agent said that it was finished, then it should finish
- Normal Edge: after the tools are invoked, it should always go back to the agent to decide what to do next
/**
* Represents an asynchronous edge action that operates on an agent state and returns a new route.
*
* @param <S> the type of the agent state
*/
public interface AsyncEdgeAction<S extends AgentState> extends Function<S, CompletableFuture<String>> {
CompletableFuture<String> apply(S t);
/**
* Creates an asynchronous edge action from a synchronous edge action.
*/
static <S extends AgentState> AsyncEdgeAction<S> edge_async(EdgeAction<S> syncAction ) { ... }
}
We can now put it all together and define the graph! (see example below)
Like default use case proposed in LangGraph blog, We have ported AgentExecutor implementation from langchain using LangGraph4j. In the agents project's module, you can the complete working code with tests. Feel free to checkout and use it as a reference.
Below you can find a piece of code of the AgentExecutor
to give you an idea of how is has built in langgraph style.
public static class State implements AgentState {
// the state's (partial) schema
static Map<String, Channel<?>> SCHEMA = mapOf(
"intermediate_steps", AppenderChannel.<IntermediateStep>of(ArrayList::new)
);
public State(Map<String, Object> initData) {
super(initData);
}
Optional<String> input() {
return value("input");
}
Optional<AgentOutcome> agentOutcome() {
return value("agent_outcome");
}
List<IntermediateStep> intermediateSteps() {
return this.<List<IntermediateStep>>value("intermediate_steps").orElseGet(emptyList());
}
}
var toolInfoList = ToolInfo.fromList( objectsWithTools );
final List<ToolSpecification> toolSpecifications = toolInfoList.stream()
.map(ToolInfo::specification)
.toList();
var agentRunnable = Agent.builder()
.chatLanguageModel(chatLanguageModel)
.tools( toolSpecifications )
.build();
// Fluent Interface
var app = new StateGraph<>(State.SCHEMA,State::new)
.addEdge(START,"agent")
.addNode( "agent", node_async( state ->
runAgent(agentRunnable, state))
)
.addNode( "action", node_async( state ->
executeTools(toolInfoList, state))
)
.addConditionalEdges(
"agent",
edge_async( state -> {
if (state.agentOutcome().map(AgentOutcome::finish).isPresent()) {
return "end";
}
return "continue";
}),
mapOf("continue", "action", "end", END)
)
.addEdge("action", "agent")
.compile();
return app.stream( inputs );