Wait for User Input

utility to render graph respresentation in PlantUML

import net.sourceforge.plantuml.SourceStringReader;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.FileFormat;
import org.bsc.langgraph4j.GraphRepresentation;

void displayDiagram( GraphRepresentation representation ) throws IOException { 
    
    var reader = new SourceStringReader(representation.getContent());

    try(var imageOutStream = new java.io.ByteArrayOutputStream()) {

        var description = reader.outputImage( imageOutStream, 0, new FileFormatOption(FileFormat.PNG));

        var imageInStream = new java.io.ByteArrayInputStream(  imageOutStream.toByteArray() );

        var image = javax.imageio.ImageIO.read( imageInStream );

        display(  image );

    }
}

Define graph with interruption

import org.bsc.langgraph4j.*;
import org.bsc.langgraph4j.prebuilt.MessagesState;
import org.bsc.langgraph4j.state.Channel;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import org.bsc.langgraph4j.action.AsyncNodeAction;
import org.bsc.langgraph4j.action.AsyncEdgeAction;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
import org.bsc.langgraph4j.checkpoint.MemorySaver;
import org.bsc.langgraph4j.CompileConfig;
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;

public class State extends MessagesState<String> {

    public State(Map<String, Object> initData) {
        super( initData  );
    }

    public Optional<String> humanFeedback() {
        return value("human_feedback");
    }

}

AsyncNodeAction<State> step1 = node_async( state -> {
    return Map.of( "messages", "Step 1" );
});

AsyncNodeAction<State> humanFeedback = node_async( state -> {
    return Map.of();
});

AsyncNodeAction<State> step3 = node_async( state -> {
    return Map.of( "messages", "Step 3" );
});

AsyncEdgeAction<State> evalHumanFeedback = edge_async( state -> {
    var feedback = state.humanFeedback().orElseThrow();
    return ( feedback.equals("next") || feedback.equals("back") ) ? feedback : "unknown";
});

var builder = new StateGraph<>(State.SCHEMA, State::new)
    .addNode("step_1", step1)
    .addNode("human_feedback", humanFeedback)
    .addNode("step_3", step3)
    .addEdge(START, "step_1")
    .addEdge("step_1", "human_feedback")
    .addConditionalEdges("human_feedback", evalHumanFeedback, 
        Map.of( "back", "step_1", "next", "step_3", "unknown", "human_feedback" ))
    .addEdge("step_3", END)
    ;

// Set up memory
var saver = new MemorySaver();

// Add
var compileConfig = CompileConfig.builder()
                        .checkpointSaver(saver)
                        .interruptBefore("human_feedback")
                        .build();

var graph = builder.compile(compileConfig);


displayDiagram( graph.getGraph(GraphRepresentation.Type.PLANTUML, "Human in the Loop", false) );
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.

png

Start graph until interruption

// Input
Map<String,Object> initialInput = Map.of("messages", "Step 0");

// Thread
var invokeConfig = RunnableConfig.builder()
                        .threadId("Thread1")
                        .build();

// Run the graph until the first interruption
for (var event : graph.stream(initialInput, invokeConfig)) {
    System.out.println(event);
}
NodeOutput{node=__START__, state={messages=[Step 0]}}
NodeOutput{node=step_1, state={messages=[Step 0, Step 1]}}

Wait for user input and update state

⚠️ The Java notebook, until now, doesn't support user input (take a look issue #39) so we could simulate input ⚠️


// We can check the state
System.out.printf("--State before update--\n%s\n", graph.getState(invokeConfig));

// Simulate user input
var userInput = "back"; // back means we want to go back to the previous node
System.out.printf("\n--User Input--\nTell me how you want to update the state: '%s'\n\n", userInput);

// We now update the state as if we are the human_feedback node
var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null);

// We can check the state
System.out.printf("--State after update--\n%s\n", graph.getState(invokeConfig) );

// We can check the next node, showing that it is node 3 (which follows human_feedback)
System.out.printf("\ngetNext()\n\twith invokeConfig:[%s]\n\twith updateConfig:[%s]\n", 
            graph.getState(invokeConfig).getNext(),  
            graph.getState(updateConfig).getNext());
;
--State before update--
StateSnapshot{node=step_1, state={messages=[Step 0, Step 1]}, config=RunnableConfig(threadId=Thread1, checkPointId=d35b4f81-0009-4998-89f2-abeb573a0efa, nextNode=human_feedback, streamMode=VALUES)}

--User Input--
Tell me how you want to update the state: 'back'

--State after update--
StateSnapshot{node=step_1, state={messages=[Step 0, Step 1], human_feedback=back}, config=RunnableConfig(threadId=Thread1, checkPointId=d35b4f81-0009-4998-89f2-abeb573a0efa, nextNode=human_feedback, streamMode=VALUES)}

getNext()
	with invokeConfig:[human_feedback]
	with updateConfig:[human_feedback]

Continue graph execution after interruption

// Continue the graph execution
for (var event : graph.stream(null, updateConfig)) {
    System.out.println(event);
}
NodeOutput{node=human_feedback, state={messages=[Step 0, Step 1], human_feedback=back}}
NodeOutput{node=step_1, state={messages=[Step 0, Step 1, Step 1], human_feedback=back}}

Waif for user input (again) and update state

⚠️ The Java notebook, until now, doesn't support user input (take a look issue #39) so we could simulate input ⚠️

var userInput = "next"; // 'next' means we want to go to the next node
System.out.printf("\n--User Input--\nTell me how you want to update the state: '%s'\n", userInput);

// We now update the state as if we are the human_feedback node
var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null);

System.out.printf("\ngetNext()\n\twith invokeConfig:[%s]\n\twith updateConfig:[%s]\n", 
            graph.getState(invokeConfig).getNext(),  
            graph.getState(updateConfig).getNext());
            ;   
--User Input--
Tell me how you want to update the state: 'next'

getNext()
	with invokeConfig:[human_feedback]
	with updateConfig:[human_feedback]

Continue graph execution after the 2nd interruption

// Continue the graph execution
for (var event : graph.stream(null, updateConfig)) {
    System.out.println(event);
}
NodeOutput{node=human_feedback, state={messages=[Step 0, Step 1, Step 1], human_feedback=next}}
NodeOutput{node=step_3, state={messages=[Step 0, Step 1, Step 1, Step 3], human_feedback=next}}
NodeOutput{node=__END__, state={messages=[Step 0, Step 1, Step 1, Step 3], human_feedback=next}}
graph.getState(updateConfig).getState();
{messages=[Step 0, Step 1, Step 1, Step 3], human_feedback=next}