diff --git a/how-tos/logging.properties b/how-tos/logging.properties new file mode 100644 index 0000000..69ca839 --- /dev/null +++ b/how-tos/logging.properties @@ -0,0 +1,21 @@ +# Set the default logging level to INFO +.level = INFO + +# Set the logging level to FINER for the package a.b.c +org.bsc.langgraph4j.level = FINE + +# Specify the handlers that will handle the log messages +handlers= java.util.logging.FileHandler + +# Configure the FileHandler +java.util.logging.FileHandler.pattern = log.txt +java.util.logging.FileHandler.limit = 0 +java.util.logging.FileHandler.count = 1 +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter + +# Set the logging level for the FileHandler to ALL +java.util.logging.FileHandler.level = ALL + +# Configure the SimpleFormatter to include date, source, logger name, level, and message +java.util.logging.SimpleFormatter.format = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %4$s %2$s %5$s%6$s%n + diff --git a/how-tos/persistence.ipynb b/how-tos/persistence.ipynb index fe9b31c..f6ecf9b 100644 --- a/how-tos/persistence.ipynb +++ b/how-tos/persistence.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -13,69 +13,19 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0mRepository \u001b[1m\u001b[32mlocal\u001b[0m url: \u001b[1m\u001b[32mfile:///Users/bsorrentino/.m2/repository/\u001b[0m added.\n", - "\u001b[0mRepositories count: 5\n", - "\u001b[0mname: \u001b[1m\u001b[32mcentral \u001b[0murl: \u001b[1m\u001b[32mhttps://repo.maven.apache.org/maven2/ \u001b[0mrelease:\u001b[32mtrue \u001b[0mupdate:\u001b[32mnever \u001b[0msnapshot:\u001b[32mfalse \u001b[0mupdate:\u001b[32mnever \n", - "\u001b[0m\u001b[0mname: \u001b[1m\u001b[32mjcenter \u001b[0murl: \u001b[1m\u001b[32mhttps://jcenter.bintray.com/ \u001b[0mrelease:\u001b[32mtrue \u001b[0mupdate:\u001b[32mnever \u001b[0msnapshot:\u001b[32mfalse \u001b[0mupdate:\u001b[32mnever \n", - "\u001b[0m\u001b[0mname: \u001b[1m\u001b[32mjboss \u001b[0murl: \u001b[1m\u001b[32mhttps://repository.jboss.org/nexus/content/repositories/releases/ \u001b[0mrelease:\u001b[32mtrue \u001b[0mupdate:\u001b[32mnever \u001b[0msnapshot:\u001b[32mfalse \u001b[0mupdate:\u001b[32mnever \n", - "\u001b[0m\u001b[0mname: \u001b[1m\u001b[32matlassian \u001b[0murl: \u001b[1m\u001b[32mhttps://packages.atlassian.com/maven/public \u001b[0mrelease:\u001b[32mtrue \u001b[0mupdate:\u001b[32mnever \u001b[0msnapshot:\u001b[32mfalse \u001b[0mupdate:\u001b[32mnever \n", - "\u001b[0m\u001b[0mname: \u001b[1m\u001b[32mlocal \u001b[0murl: \u001b[1m\u001b[32mfile:///Users/bsorrentino/.m2/repository/ \u001b[0mrelease:\u001b[32mtrue \u001b[0mupdate:\u001b[32mnever \u001b[0msnapshot:\u001b[32mtrue \u001b[0mupdate:\u001b[32malways \n", - "\u001b[0m" - ] - } - ], + "outputs": [], "source": [ "%dependency /add-repo local \\{localRespoUrl} release|never snapshot|always\n", - "%dependency /list-repos\n" + "%dependency /list-repos" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Adding dependency \u001b[0m\u001b[1m\u001b[32morg.bsc.langgraph4j:langgraph4j-core-jdk8:1.0-SNAPSHOT\n", - "\u001b[0mAdding dependency \u001b[0m\u001b[1m\u001b[32mdev.langchain4j:langchain4j:0.33.0\n", - "\u001b[0mAdding dependency \u001b[0m\u001b[1m\u001b[32mdev.langchain4j:langchain4j-open-ai:0.33.0\n", - "\u001b[0mSolving dependencies\n", - "Resolved artifacts count: 22\n", - "Add to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/bsc/langgraph4j/langgraph4j-core-jdk8/1.0-SNAPSHOT/langgraph4j-core-jdk8-1.0-SNAPSHOT.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/bsc/async/async-generator-jdk8/2.0.1/async-generator-jdk8-2.0.1.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/dev/langchain4j/langchain4j/0.33.0/langchain4j-0.33.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/dev/langchain4j/langchain4j-core/0.33.0/langchain4j-core-0.33.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/google/code/gson/gson/2.10.1/gson-2.10.1.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/apache/opennlp/opennlp-tools/1.9.4/opennlp-tools-1.9.4.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jsoup/jsoup/1.16.1/jsoup-1.16.1.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/dev/langchain4j/langchain4j-open-ai/0.33.0/langchain4j-open-ai-0.33.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/dev/ai4j/openai4j/0.17.0/openai4j-0.17.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/retrofit2/retrofit/2.9.0/retrofit-2.9.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/retrofit2/converter-gson/2.9.0/converter-gson-2.9.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/okio/okio/3.6.0/okio-3.6.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/okio/okio-jvm/3.6.0/okio-jvm-3.6.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.10/kotlin-stdlib-common-1.9.10.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.8.21/kotlin-stdlib-jdk8-1.8.21.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jetbrains/kotlin/kotlin-stdlib/1.8.21/kotlin-stdlib-1.8.21.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jetbrains/annotations/13.0/annotations-13.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.8.21/kotlin-stdlib-jdk7-1.8.21.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/squareup/okhttp3/okhttp-sse/4.12.0/okhttp-sse-4.12.0.jar\u001b[0m\n", - "\u001b[0mAdd to classpath: \u001b[0m\u001b[32m/Users/bsorrentino/Library/Jupyter/kernels/rapaio-jupyter-kernel/mima_cache/com/knuddels/jtokkit/1.1.0/jtokkit-1.1.0.jar\u001b[0m\n", - "\u001b[0m" - ] - } - ], + "outputs": [], "source": [ "%dependency /add org.bsc.langgraph4j:langgraph4j-core-jdk8:1.0-SNAPSHOT\n", "%dependency /add dev.langchain4j:langchain4j:\\{langchain4jVersion}\n", @@ -420,19 +370,15 @@ " \n", " var lastMessage = state.lastMessage();\n", " \n", - " if ( !lastMessage.isPresent()) {\n", - " return \"exit\";\n", - " }\n", + " if ( !lastMessage.isPresent()) return \"exit\";\n", "\n", " var message = (AiMessage)lastMessage.get();\n", "\n", - " // If no tools are called, we can finish (respond to the user)\n", - " if ( !message.hasToolExecutionRequests() ) {\n", - " return \"exit\";\n", - " }\n", + " // If tools should be called\n", + " if ( message.hasToolExecutionRequests() ) return \"next\";\n", " \n", - " // Otherwise if there is, we continue and call the tools\n", - " return \"next\";\n", + " // If no tools are called, we can finish (respond to the user)\n", + " return \"exit\";\n", "};\n", "\n", "var llm = new LLM();\n", diff --git a/how-tos/time-travel.ipynb b/how-tos/time-travel.ipynb new file mode 100644 index 0000000..21e9e78 --- /dev/null +++ b/how-tos/time-travel.ipynb @@ -0,0 +1,869 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "String userHomeDir = System.getProperty(\"user.home\");\n", + "String localRespoUrl = \"file://\" + userHomeDir + \"/.m2/repository/\";\n", + "String langchain4jVersion = \"0.33.0\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%dependency /add-repo local \\{localRespoUrl} release|never snapshot|always\n", + "%dependency /list-repos" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%dependency /add org.bsc.langgraph4j:langgraph4j-core-jdk8:1.0-SNAPSHOT\n", + "%dependency /add dev.langchain4j:langchain4j:\\{langchain4jVersion}\n", + "%dependency /add dev.langchain4j:langchain4j-open-ai:\\{langchain4jVersion}\n", + "\n", + "%dependency /resolve" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import java.io.FileInputStream;\n", + "import java.io.IOException;\n", + "import java.util.logging.LogManager;\n", + "import java.util.logging.Logger;\n", + "\n", + "try( FileInputStream configFile = new FileInputStream(\"./logging.properties\") ) {\n", + " var lm = LogManager.getLogManager();\n", + " lm.checkAccess();\n", + " lm.readConfiguration(configFile);\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to view and update past graph state\n", + "\n", + "Once you start [checkpointing](./persistence.ipynb) your graphs, you can easily\n", + "**get** or **update** the state of the agent at any point in time. This permits\n", + "a few things:\n", + "\n", + "1. You can surface a state during an interrupt to a user to let them accept an\n", + " action.\n", + "2. You can **rewind** the graph to reproduce or avoid issues.\n", + "3. You can **modify** the state to embed your agent into a larger system, or to\n", + " let the user better control its actions.\n", + "\n", + "The key methods used for this functionality are:\n", + "\n", + "- [getState](/langgraphjs/reference/classes/langgraph_pregel.Pregel.html#getState):\n", + " fetch the values from the target config\n", + "- [updateState](/langgraphjs/reference/classes/langgraph_pregel.Pregel.html#updateState):\n", + " apply the given values to the target state\n", + "\n", + "**Note:** this requires passing in a checkpointer.\n", + "\n", + "\n", + "\n", + "This works for [StateGraph](https://bsorrentino.github.io/langgraph4j/apidocs/org/bsc/langgraph4j/StateGraph.html)\n", + "\n", + "Below is an example." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the state\n", + "\n", + "State is an (immutable) data class, inheriting from [AgentState], shared with all nodes in our graph. A state is basically a wrapper of a `Map` that provides some enhancers:\n", + "\n", + "1. Schema (optional), that is a `Map` where each [Channel] describe behaviour of the related property\n", + "1. `value()` accessors that inspect Map an return an Optional of value contained and cast to the required type\n", + "\n", + "[Channel]: https://bsorrentino.github.io/langgraph4j/apidocs/org/bsc/langgraph4j/state/Channel.html\n", + "[AgentState]: https://bsorrentino.github.io/langgraph4j/apidocs/org/bsc/langgraph4j/state/AgentState.html" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import org.bsc.langgraph4j.state.AgentState;\n", + "import org.bsc.langgraph4j.state.Channel;\n", + "import org.bsc.langgraph4j.state.AppenderChannel;\n", + "import dev.langchain4j.data.message.AiMessage;\n", + "import dev.langchain4j.data.message.ChatMessage;\n", + "\n", + "public class MessageState extends AgentState {\n", + "\n", + " static Map> SCHEMA = Map.of(\n", + " \"messages\", AppenderChannel.of(ArrayList::new)\n", + " );\n", + "\n", + " public MessageState(Map initData) {\n", + " super( initData );\n", + " }\n", + "\n", + " List messages() {\n", + " return this.>value( \"messages\" )\n", + " .orElseThrow( () -> new RuntimeException( \"messages not found\" ) );\n", + " }\n", + "\n", + " // utility method to quick access to last message\n", + " Optional lastMessage() {\n", + " List messages = messages();\n", + " return ( messages.isEmpty() ) ? \n", + " Optional.empty() :\n", + " Optional.of(messages.get( messages.size() - 1 ));\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Serializers\n", + "\n", + "Every object that should be stored into State **MUST BE SERIALIZABLE**. If the object is not `Serializable` by default, Langgraph4j provides a way to build and associate a custom [Serializer] to it. \n", + "\n", + "In the example, since [AiMessage] and [UserMessage] from Langchain4j are not Serialzable we have to create an register a new custom [`Serializer`].\n", + "\n", + "[Serializer]: https://bsorrentino.github.io/langgraph4j/apidocs/org/bsc/langgraph4j/serializer/Serializer.html\n", + "[AiMessage]: https://docs.langchain4j.dev/apidocs/dev/langchain4j/data/message/AiMessage.html\n", + "[UserMessage]: https://docs.langchain4j.dev/apidocs/dev/langchain4j/data/message/UserMessage.html" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import org.bsc.langgraph4j.serializer.Serializer;\n", + "import org.bsc.langgraph4j.serializer.BaseSerializer;\n", + "import org.bsc.langgraph4j.serializer.StateSerializer;\n", + "import dev.langchain4j.data.message.AiMessage;\n", + "import dev.langchain4j.data.message.UserMessage;\n", + "import dev.langchain4j.data.message.ChatMessageType;\n", + "import dev.langchain4j.agent.tool.ToolExecutionRequest;\n", + "import dev.langchain4j.data.message.ToolExecutionResultMessage;\n", + "\n", + "// Setup custom serializer for Langchain4j ToolExecutionRequest\n", + "StateSerializer.register(ToolExecutionRequest.class, new Serializer() {\n", + "\n", + " @Override\n", + " public void write(ToolExecutionRequest object, ObjectOutput out) throws IOException {\n", + " out.writeUTF( object.id() );\n", + " out.writeUTF( object.name() );\n", + " out.writeUTF( object.arguments() );\n", + " }\n", + "\n", + " @Override\n", + " public ToolExecutionRequest read(ObjectInput in) throws IOException, ClassNotFoundException {\n", + " return ToolExecutionRequest.builder()\n", + " .id(in.readUTF())\n", + " .name(in.readUTF())\n", + " .arguments(in.readUTF())\n", + " .build();\n", + " }\n", + "});\n", + "\n", + "// Setup custom serializer for Langchain4j AiMessage\n", + "StateSerializer.register(ChatMessage.class, new BaseSerializer() {\n", + "\n", + " void writeAI( AiMessage msg, ObjectOutput out) throws IOException {\n", + " var hasToolExecutionRequests = msg.hasToolExecutionRequests();\n", + "\n", + " out.writeBoolean( hasToolExecutionRequests );\n", + " \n", + " if( hasToolExecutionRequests ) {\n", + " writeObjectWithSerializer( msg.toolExecutionRequests(), out);\n", + " }\n", + " else {\n", + " out.writeUTF(msg.text());\n", + " } \n", + " }\n", + "\n", + " AiMessage readAI( ObjectInput in) throws IOException, ClassNotFoundException {\n", + " var hasToolExecutionRequests = in.readBoolean();\n", + " if( hasToolExecutionRequests ) {\n", + " List toolExecutionRequests = readObjectWithSerializer(in);\n", + " return AiMessage.aiMessage( toolExecutionRequests );\n", + " }\n", + " return AiMessage.aiMessage(in.readUTF());\n", + " }\n", + " \n", + " void writeUSER( UserMessage msg, ObjectOutput out) throws IOException {\n", + " out.writeUTF( msg.text() ); \n", + " }\n", + "\n", + " UserMessage readUSER( ObjectInput in) throws IOException, ClassNotFoundException {\n", + " return UserMessage.from( in.readUTF() );\n", + " }\n", + "\n", + " void writeEXREQ( ToolExecutionResultMessage msg, ObjectOutput out ) throws IOException {\n", + " out.writeUTF( msg.id() );\n", + " out.writeUTF( msg.toolName() );\n", + " out.writeUTF( msg.text() );\n", + " }\n", + "\n", + " ToolExecutionResultMessage readEXREG( ObjectInput in ) throws IOException, ClassNotFoundException {\n", + " return new ToolExecutionResultMessage( in.readUTF(), in.readUTF(),in.readUTF() );\n", + " }\n", + "\n", + "\n", + " @Override\n", + " public void write(ChatMessage object, ObjectOutput out) throws IOException {\n", + " out.writeObject( object.type() );\n", + " switch( object.type() ) {\n", + " case AI -> writeAI((AiMessage)object, out );\n", + " case USER -> writeUSER( (UserMessage)object, out );\n", + " case TOOL_EXECUTION_RESULT -> writeEXREQ( (ToolExecutionResultMessage)object, out );\n", + " case SYSTEM -> {\n", + " // Nothing\n", + " }\n", + " };\n", + " }\n", + "\n", + " @Override\n", + " public ChatMessage read(ObjectInput in) throws IOException, ClassNotFoundException {\n", + "\n", + " ChatMessageType type = (ChatMessageType)in.readObject();\n", + "\n", + " return switch( type ) {\n", + " case AI -> { yield readAI( in ); }\n", + " case USER -> { yield readUSER( in ); }\n", + " case TOOL_EXECUTION_RESULT -> { yield readEXREG( in ); }\n", + " case SYSTEM -> {\n", + " yield null;\n", + " }\n", + " };\n", + " }\n", + "});\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up the tools\n", + "\n", + "Using [langchain4j], We will first define the tools we want to use. For this simple example, we will\n", + "use create a placeholder search engine. However, it is really easy to create\n", + "your own tools - see documentation\n", + "[here][tools] on how to do\n", + "that.\n", + "\n", + "[langchain4j]: https://docs.langchain4j.dev\n", + "[tools]: https://docs.langchain4j.dev/tutorials/tools" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import dev.langchain4j.agent.tool.P;\n", + "import dev.langchain4j.agent.tool.Tool;\n", + "\n", + "import java.util.Optional;\n", + "\n", + "import static java.lang.String.format;\n", + "\n", + "public class SearchTool {\n", + "\n", + " @Tool(\"Use to surf the web, fetch current information, check the weather, and retrieve other information.\")\n", + " String execQuery(@P(\"The query to use in your search.\") String query) {\n", + "\n", + " // This is a placeholder for the actual implementation\n", + " return \"Cold, with a low of 13 degrees\";\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Set up the model\n", + "\n", + "Now we will load the\n", + "[chat model].\n", + "\n", + "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", + "2. It should work with [tool calling],meaning it can return function arguments in its response.\n", + "\n", + "Note:\n", + " >\n", + " > These model requirements are not general requirements for using LangGraph4j - they are just requirements for this one example.\n", + " >\n", + "\n", + "[chat model]: https://docs.langchain4j.dev/tutorials/chat-and-language-models\n", + "[tool calling]: https://docs.langchain4j.dev/tutorials/tools \n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "SLF4J: No SLF4J providers were found.\n", + "SLF4J: Defaulting to no-operation (NOP) logger implementation\n", + "SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.\n" + ] + } + ], + "source": [ + "import dev.langchain4j.model.openai.OpenAiChatModel;\n", + "import dev.langchain4j.agent.tool.ToolSpecification;\n", + "import dev.langchain4j.agent.tool.ToolSpecifications;\n", + "\n", + "OpenAiChatModel llm = OpenAiChatModel.builder()\n", + " .apiKey( System.getenv(\"OPENAI_API_KEY\") )\n", + " .modelName( \"gpt-4o\" )\n", + " .logResponses(true)\n", + " .maxRetries(2)\n", + " .temperature(0.0)\n", + " .maxTokens(2000)\n", + " .build() \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Test function calling" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_3YrelSH5At2zVambJLLf8gYo\", name = \"execQuery\", arguments = \"{\"query\":\"London weather forecast for tomorrow\"}\" }] }\n" + ] + }, + { + "data": { + "text/plain": [ + "Cold, with a low of 13 degrees" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import dev.langchain4j.agent.tool.ToolSpecification;\n", + "import dev.langchain4j.agent.tool.ToolSpecifications;\n", + "import dev.langchain4j.data.message.UserMessage;\n", + "import dev.langchain4j.data.message.AiMessage;\n", + "import dev.langchain4j.model.output.Response;\n", + "import dev.langchain4j.model.openai.OpenAiChatModel;\n", + "import dev.langchain4j.service.tool.DefaultToolExecutor;\n", + "\n", + "var tools = ToolSpecifications.toolSpecificationsFrom( SearchTool.class );\n", + "\n", + "UserMessage userMessage = UserMessage.from(\"What will the weather be like in London tomorrow?\");\n", + "Response response = llm.generate(Collections.singletonList(userMessage), tools );\n", + "AiMessage aiMessage = response.content();\n", + "\n", + "System.out.println( aiMessage );\n", + "\n", + "var executionRequest = aiMessage.toolExecutionRequests().get(0);\n", + "\n", + "var executor = new DefaultToolExecutor( new SearchTool(), executionRequest );\n", + "\n", + "var result = executor.execute( executionRequest, null );\n", + "\n", + "result;\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define the graph\n", + "\n", + "We can now put it all together. We will run it first without a checkpointer:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import static org.bsc.langgraph4j.StateGraph.START;\n", + "import static org.bsc.langgraph4j.StateGraph.END;\n", + "import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;\n", + "import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;\n", + "import org.bsc.langgraph4j.StateGraph;\n", + "import org.bsc.langgraph4j.action.EdgeAction;\n", + "import org.bsc.langgraph4j.action.NodeAction;\n", + "import dev.langchain4j.data.message.AiMessage;\n", + "import dev.langchain4j.data.message.ChatMessage;\n", + "import dev.langchain4j.service.tool.DefaultToolExecutor;\n", + "import org.bsc.langgraph4j.checkpoint.MemorySaver; \n", + "import org.bsc.langgraph4j.CompileConfig; \n", + "import java.util.stream.Collectors;\n", + "// Route Message \n", + "EdgeAction routeMessage = state -> {\n", + " \n", + " var lastMessage = state.lastMessage();\n", + " \n", + " if ( !lastMessage.isPresent()) return \"exit\";\n", + "\n", + " if( lastMessage.get() instanceof AiMessage message ) {\n", + "\n", + " // If tools should be called\n", + " if ( message.hasToolExecutionRequests() ) return \"next\";\n", + " \n", + " }\n", + " \n", + " // If no tools are called, we can finish (respond to the user)\n", + " return \"exit\";\n", + "};\n", + "\n", + "// Call Model\n", + "NodeAction callModel = state -> {\n", + " var tools = ToolSpecifications.toolSpecificationsFrom( SearchTool.class );\n", + "\n", + " var response = llm.generate( (List)state.messages(), tools );\n", + "\n", + " return Map.of( \"messages\", response.content() );\n", + "};\n", + "\n", + "// Invoke Tool \n", + "NodeAction invokeTool = state -> {\n", + " var lastMessage = (AiMessage)state.lastMessage()\n", + " .orElseThrow( () -> ( new IllegalStateException( \"last message not found!\")) );\n", + "\n", + " var executionRequest = lastMessage.toolExecutionRequests().get(0);\n", + " \n", + " var executor = new DefaultToolExecutor( new SearchTool(), executionRequest );\n", + "\n", + " var result = executor.execute( executionRequest, null );\n", + "\n", + " return Map.of( \"messages\", new ToolExecutionResultMessage( executionRequest.id(), executionRequest.name(), result ) );\n", + "};\n", + "\n", + "// Define Graph\n", + "var workflow = new StateGraph ( MessageState.SCHEMA, MessageState::new )\n", + " .addNode(\"agent\", node_async(callModel) )\n", + " .addNode(\"tools\", node_async(invokeTool) )\n", + " .addEdge(START, \"agent\")\n", + " .addConditionalEdges(\"agent\", edge_async(routeMessage), Map.of( \"next\", \"tools\", \"exit\", END ))\n", + " .addEdge(\"tools\", \"agent\");\n", + "\n", + "// Here we only save in-memory\n", + "var memory = new MemorySaver();\n", + "\n", + "var compileConfig = CompileConfig.builder()\n", + " .checkpointSaver(memory)\n", + " .build();\n", + "\n", + "var graph = workflow.compile(compileConfig);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interacting with the Agent\n", + "\n", + "We can now interact with the agent. Between interactions you can get and update state." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__START__\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"Hi I'm Bartolo.\" }] }]}\n", + "agent\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"Hi I'm Bartolo.\" }] }, AiMessage { text = \"Hello Bartolo! How can I assist you today?\" toolExecutionRequests = null }]}\n", + "__END__\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"Hi I'm Bartolo.\" }] }, AiMessage { text = \"Hello Bartolo! How can I assist you today?\" toolExecutionRequests = null }]}\n" + ] + } + ], + "source": [ + "import org.bsc.langgraph4j.RunnableConfig;\n", + "\n", + "var runnableConfig = RunnableConfig.builder()\n", + " .threadId(\"conversation-num-1\" )\n", + " .build();\n", + "\n", + "Map inputs = Map.of( \"messages\", UserMessage.from(\"Hi I'm Bartolo.\") );\n", + "\n", + "var result = graph.stream( inputs, runnableConfig );\n", + "\n", + "for( var r : result ) {\n", + " System.out.println( r.node() );\n", + " System.out.println( r.state() );\n", + " \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here you can see the \"`agent`\" node ran, and then our edge returned `__END__` so the graph stopped execution there.\n", + "\n", + "Let's check the current graph state." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"Hi I'm Bartolo.\" }] }, AiMessage { text = \"Hello Bartolo! How can I assist you today?\" toolExecutionRequests = null }]}, config=RunnableConfig(threadId=conversation-num-1, checkPointId=93c2b047-7927-4062-89e0-4498f63813f6, nextNode=__END__))\n" + ] + } + ], + "source": [ + "import org.bsc.langgraph4j.checkpoint.Checkpoint;\n", + "\n", + "var checkpoint = graph.getState(runnableConfig);\n", + "\n", + "System.out.println(checkpoint);\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The current state is the two messages we've seen above, 1. the Human Message we sent in, 2. the AIMessage we got back from the model.\n", + "\n", + "The next value is `__END__` since the graph has terminated." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "__END__" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "checkpoint.getNext()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Let's get it to execute a tool\n", + "\n", + "When we call the graph again, it will create a checkpoint after each internal execution step. Let's get it to run a tool, then look at the checkpoint." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius. Is there anything else you need help with?\" toolExecutionRequests = null }\n" + ] + } + ], + "source": [ + "\n", + "Map inputs = Map.of( \"messages\", UserMessage.from(\"What's the weather like in SF currently?\") );\n", + "\n", + "var state = graph.invoke( inputs, runnableConfig ).orElseThrow( () ->(new IllegalStateException()) ) ;\n", + "\n", + "System.out.println( state.lastMessage().orElse(null) );\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pause before tools\n", + "\n", + "If you notice below, we now will add interruptBefore=[\"action\"] - this means that before any actions are taken we pause. This is a great moment to allow the user to correct and update the state! This is very useful when you want to have a human-in-the-loop to validate (and potentially change) the action to take.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "__START__\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }]}\n", + "agent\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }]}\n" + ] + } + ], + "source": [ + "var memory = new MemorySaver();\n", + "\n", + "var compileConfig = CompileConfig.builder()\n", + " .checkpointSaver(memory)\n", + " .interruptBefore( \"tools\")\n", + " .build();\n", + "\n", + "var graphWithInterrupt = workflow.compile(compileConfig);\n", + "\n", + "var runnableConfig = RunnableConfig.builder()\n", + " .threadId(\"conversation-2\" )\n", + " .build();\n", + "\n", + "Map inputs = Map.of( \"messages\", UserMessage.from(\"What's the weather like in SF currently?\") );\n", + "\n", + "var result = graphWithInterrupt.stream( inputs, runnableConfig );\n", + "\n", + "for( var r : result ) {\n", + " System.out.println( r.node() );\n", + " System.out.println( r.state() );\n", + " \n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get State\n", + "\n", + "You can fetch the latest graph checkpoint using `getState(config)`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tools" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "var snapshot = graphWithInterrupt.getState(runnableConfig);\n", + "snapshot.getNext();\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Resume\n", + "\n", + "You can resume by running the graph with a null input. The checkpoint is loaded, and with no new inputs, it will execute as if no interrupt had occurred." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tools\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }]}\n", + "agent\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}\n", + "__END__\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}\n" + ] + } + ], + "source": [ + "var result = graphWithInterrupt.stream( null, snapshot.getConfig() );\n", + "\n", + "for( var r : result ) {\n", + " System.out.println( r.node() );\n", + " System.out.println( r.state() );\n", + " \n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Check full history\n", + "\n", + "Let's browse the history of this thread, from newest to oldest." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=5bb993e3-8703-48c9-8284-1ee801c265b7, nextNode=__END__))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=11fc5618-c5b2-4b15-843c-8a51267c76a1, nextNode=agent))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=ab776d0f-a051-424c-9a70-6017f3bfdbf8, nextNode=__END__))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=d6909dae-6f94-4afe-a8b5-7b39da95cc22, nextNode=agent))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=0bb4e2ce-67fe-4585-94c7-035602f51f16, nextNode=tools))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=0167fa6f-e3b1-4b9e-aeac-b8ff15f191ba, nextNode=tools))\n", + "--\n", + "StateSnapshot(state={messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }]}, config=RunnableConfig(threadId=conversation-2, checkPointId=3a4cc3c3-e863-4799-b33c-8ca295000552, nextNode=agent))\n", + "--\n" + ] + } + ], + "source": [ + "RunnableConfig toReplay = null;\n", + "var states = graphWithInterrupt.getStateHistory(runnableConfig);\n", + "for( var state: states ) {\n", + " \n", + " System.out.println(state);\n", + " System.out.println(\"--\");\n", + "\n", + " if (state.getState().messages().size() == 3) {\n", + " toReplay = state.getConfig();\n", + " }\n", + "}\n", + "if (toReplay==null) {\n", + " throw new IllegalStateException(\"No state to replay\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Replay a past state\n", + "\n", + "To replay from this place we just need to pass its config back to the agent." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "agent\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}\n", + "__END__\n", + "{messages=[UserMessage { name = null contents = [TextContent { text = \"What's the weather like in SF currently?\" }] }, AiMessage { text = null toolExecutionRequests = [ToolExecutionRequest { id = \"call_M60jubFO7QzVUGZsutRsvdjw\", name = \"execQuery\", arguments = \"{\"query\":\"current weather in San Francisco\"}\" }] }, ToolExecutionResultMessage { id = \"call_M60jubFO7QzVUGZsutRsvdjw\" toolName = \"execQuery\" text = \"Cold, with a low of 13 degrees\" }, AiMessage { text = \"The current weather in San Francisco is cold, with a low of 13 degrees Celsius.\" toolExecutionRequests = null }]}\n" + ] + } + ], + "source": [ + "var results = graphWithInterrupt.stream(null, toReplay ); \n", + "\n", + "for( var r : results ) {\n", + " System.out.println( r.node() );\n", + " System.out.println( r.state() );\n", + " \n", + "}\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Java (rjk 2.1.0)", + "language": "java", + "name": "rapaio-jupyter-kernel" + }, + "language_info": { + "codemirror_mode": "java", + "file_extension": ".jshell", + "mimetype": "text/x-java-source", + "name": "java", + "nbconvert_exporter": "script", + "pygments_lexer": "java", + "version": "22.0.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}