Skip to content

Commit

Permalink
refactor(how-tos): wait-user-input refinemnts
Browse files Browse the repository at this point in the history
- add conditional edges to handle user input

resolve discussion #84
  • Loading branch information
bsorrentino committed Feb 26, 2025
1 parent 66dc09a commit 3eb7b15
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 259 deletions.
14 changes: 8 additions & 6 deletions how-tos/convertnb.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#!/bin/bash

# Activate the virtual environment
source .venv/bin/activate

# Loop through all Jupyter notebooks in the current directory
for notebook in *.ipynb; do
# Convert the notebook to markdown
jupyter nbconvert --to markdown "$notebook" --output-dir=src/site/markdown
done
# Set the notebook argument, defaulting to all .ipynb files if not provided
NB="${1:-*.ipynb}"

# Convert the specified notebook(s) to markdown
jupyter nbconvert --to markdown $NB --output-dir=src/site/markdown

echo "Conversion complete!"
echo "Conversion complete!"
2 changes: 1 addition & 1 deletion how-tos/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-parent</artifactId>
<version>1.4.1</version>
<version>1.4-SNAPSHOT</version>
</parent>

<artifactId>langgraph4j-howtos</artifactId>
Expand Down
220 changes: 130 additions & 90 deletions how-tos/src/site/markdown/wait-user-input.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
# Wait for User Input


**utility to render graph respresentation in PlantUML**


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

static java.awt.Image plantUML2PNG( String code ) throws IOException {
var reader = new SourceStringReader(code);
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() );

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

display( image );

}
}

```

## Define graph with interruption


```java
import org.bsc.langgraph4j.*;
Expand All @@ -31,111 +40,87 @@ import org.bsc.langgraph4j.state.AppenderChannel;
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.utils.CollectionsUtils.mapOf;
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 {
public class State extends MessagesState<String> {

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

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

}

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

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

AsyncNodeAction<State> humanFeedback = node_async(state -> {
System.out.println( "---human_feedback---" );
return mapOf();
AsyncNodeAction<State> step3 = node_async( state -> {
return Map.of( "messages", "Step 3" );
});

AsyncNodeAction<State> step3 = node_async(state -> {
System.out.println( "---Step 3---" );
return mapOf();
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);
builder.addNode("step_1", step1);
builder.addNode("human_feedback", humanFeedback);
builder.addNode("step_3", step3);
builder.addEdge(START, "step_1");
builder.addEdge("step_1", "human_feedback");
builder.addEdge("human_feedback", "step_3");
builder.addEdge("step_3", END);
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);

// View as PlantUML
var plantuml = graph.getGraph(GraphRepresentation.Type.PLANTUML).getContent();

display( plantuml );
display( plantUML2PNG(plantuml) );
```

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.

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

var graph = builder.compile(compileConfig);

@startuml Graph_Diagram
skinparam usecaseFontSize 14
skinparam usecaseStereotypeFontSize 12
skinparam hexagonFontSize 14
skinparam hexagonStereotypeFontSize 12
title "Graph Diagram"
footer

powered by langgraph4j
end footer
circle start<<input>> as __START__
circle stop as __END__
usecase "step_1"<<Node>>
usecase "human_feedback"<<Node>>
usecase "step_3"<<Node>>
"__START__" -down-> "step_1"
"step_1" -down-> "human_feedback"
"human_feedback" -down-> "step_3"
"step_3" -down-> "__END__"
@enduml

displayDiagram( graph.getGraph(GraphRepresentation.Type.PLANTUML, "Human in the Loop", false) );

```



![png](wait-user-input_files/wait-user-input_4_2.png)
![png](wait-user-input_files/wait-user-input_7_0.png)






c1ce6176-a34b-4110-953e-0923f3918f51


## Start graph until interruption


```java
// Input
var initialInput = mapOf("input", (Object) "hello world");
Map<String,Object> initialInput = Map.of("messages", "Step 0");

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

// Run the graph until the first interruption
for (var event : graph.stream(initialInput, invokeConfig)) {
Expand All @@ -144,37 +129,94 @@ for (var event : graph.stream(initialInput, invokeConfig)) {

```

---Step 1---
NodeOutput{node=__START__, state={input=hello world, messages=[]}}
NodeOutput{node=step_1, state={input=hello world, messages=[]}}
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](https://github.com/padreati/rapaio-jupyter-kernel/issues/39)) so we could simulate input ⚠️
```java
// Get user input
//String userInput = new Scanner(System.in).nextLine();
String userInput = "go to step 3!";
System.out.println("Tell me how you want to update the state: " + userInput);
// 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, mapOf("user_feedback", userInput), "human_feedback");
var updateConfig = graph.updateState(invokeConfig, mapOf("user_feedback", userInput), null);
var updateConfig = graph.updateState(invokeConfig, Map.of("human_feedback", userInput), null);
// We can check the state
System.out.println("--State after update--");
System.out.println(graph.getState(invokeConfig));
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.println("getNext with invokeConfig: " + graph.getState(invokeConfig).getNext());
System.out.println("getNext with updateConfig: " + graph.getState(updateConfig).getNext());
System.out.printf("\ngetNext()\n\twith invokeConfig:[%s]\n\twith updateConfig:[%s]\n",
graph.getState(invokeConfig).getNext(),
graph.getState(updateConfig).getNext());
;
```
Tell me how you want to update the state: go to step 3!
--State before update--
StateSnapshot{node=step_1, state={messages=[Step 0, Step 1]}, config=RunnableConfig(threadId=Thread1, checkPointId=7bb95b67-c26d-485a-a8db-9ce20a0ea39f, 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={user_feedback=go to step 3!, input=hello world, messages=[]}, config=RunnableConfig(threadId=Thread1, checkPointId=5a31577e-2b4a-4db8-a969-9a58dae4a080, nextNode=human_feedback, streamMode=VALUES)}
getNext with invokeConfig: human_feedback
getNext with updateConfig: human_feedback
StateSnapshot{node=step_1, state={messages=[Step 0, Step 1], human_feedback=back}, config=RunnableConfig(threadId=Thread1, checkPointId=7bb95b67-c26d-485a-a8db-9ce20a0ea39f, nextNode=human_feedback, streamMode=VALUES)}
getNext()
with invokeConfig:[human_feedback]
with updateConfig:[human_feedback]
## Continue graph execution after interruption
```java
// 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](https://github.com/padreati/rapaio-jupyter-kernel/issues/39)) so we could simulate input ⚠️


```java
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


```java
Expand All @@ -184,11 +226,9 @@ for (var event : graph.stream(null, updateConfig)) {
}
```

---human_feedback---
---Step 3---
NodeOutput{node=human_feedback, state={user_feedback=go to step 3!, input=hello world, messages=[]}}
NodeOutput{node=step_3, state={user_feedback=go to step 3!, input=hello world, messages=[]}}
NodeOutput{node=__END__, state={user_feedback=go to step 3!, input=hello world, messages=[]}}
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}}



Expand All @@ -199,6 +239,6 @@ graph.getState(updateConfig).getState();



{user_feedback=go to step 3!, input=hello world, messages=[]}
{messages=[Step 0, Step 1, Step 1, Step 3], human_feedback=next}


Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3eb7b15

Please sign in to comment.