{ "cells": [ { "cell_type": "markdown", "id": "java-intro", "metadata": {}, "source": [ "# Building a Java Multi-Turn, Multi-Agent Conversational System with LangGraph4j\n", "\n", "Welcome to the tutorial for building java agentic apps powered by LangChain4j and LangGraph4j.\n", "\n", "We will build a conversational application with two specialized agents: a career advisor and an education advisor. The graph keeps the conversation history in an in-memory checkpoint and hands the conversation between advisors when the user's intent changes.\n", "\n", "You will learn how to:\n", "\n", "* Define LangChain4j tools in Java\n", "* Wrap specialized agents in LangGraph4j nodes\n", "* Pause between turns while retaining conversation state\n", "* Route a follow-up turn to another agent using the same thread ID" ] }, { "cell_type": "markdown", "id": "java-prerequisites", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "Install the following before running the notebook:\n", "\n", "* **Java 11 or newer**. Check with `java -version`.\n", "* **Python and JupyterLab or Jupyter Notebook**. On macOS, the Jupyter documentation's Homebrew recipe is `brew install jupyter`. On Windows, install Python from [python.org](https://www.python.org/downloads/windows/) and then run `pip install jupyterlab` (or `pip install notebook`).\n", "* **The JJava kernel**. Download `jjava-${version}-kernelspec.zip` from the [JJava GitHub releases](https://github.com/dflib/jjava/releases), unzip it, and install the kernel from the directory containing the unzipped folder:\n", "\n", "```bash\n", "jupyter kernelspec install jjava-${version}-kernelspec --user --name=java\n", "```\n", "\n", "* **Maven 3.9 or newer**, available as `mvn` on `PATH`. The dependency cell uses it to download LangGraph4j, LangChain4j, and their transitive dependencies.\n", "* **Network access to Maven Central** for the first dependency download.\n", "* An `OPENAI_API_KEY` environment variable. The key is read by Java and is never stored in this notebook.\n", "\n", "Verify the installation with `jupyter kernelspec list`, then start Jupyter with `jupyter lab` or `jupyter notebook` and select the **Java (jjava)** kernel. If another Java kernel is already installed under the same name, remove it first with `jupyter kernelspec remove java`.\n", "\n", "These requirements follow the [JJava prerequisites](https://dflib.org/jjava/docs/1.x/#_prerequisites)." ] }, { "cell_type": "markdown", "id": "java-step-1", "metadata": {}, "source": [ "### **Step 1: Prepare the Java dependencies**\n", "\n", "This notebook uses the Java (jjava) kernel and the LangGraph4j/LangChain4j libraries. The tutorial implementation itself is embedded below; only the third-party JARs need to be placed on the kernel classpath. Run the next Java cell once from this notebook. It writes a standalone Maven POM and downloads the dependencies without relying on the repository's Java source tree.\n", "\n", "```bash\n", "mkdir -p java-notebook-dependencies\n", "cat > java-notebook-dependencies/pom.xml <<'EOF'\n", "\n", "\n", " 4.0.0\n", " local.notebook\n", " langgraph4j-notebook-dependencies\n", " 1.0.0\n", " \n", " \n", " \n", " dev.langchain4j\n", " langchain4j-bom\n", " 1.19.0\n", " pom\n", " import\n", " \n", " \n", " \n", " \n", " \n", " org.bsc.langgraph4j\n", " langgraph4j-agent-executor\n", " 1.8.26\n", " \n", " \n", " org.bsc.langgraph4j\n", " langgraph4j-langchain4j\n", " 1.8.26\n", " \n", " \n", " dev.langchain4j\n", " langchain4j-open-ai\n", " \n", " \n", "\n", "EOF\n", "mvn -f java-notebook-dependencies/pom.xml dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=lib\n", "```\n", "\n", "The standalone POM downloads the direct and transitive dependencies from Maven Central into `java-notebook-dependencies/lib`. The optional shell commands above are equivalent to the runnable Java cell. The following classpath cell adds those JARs to jjava. No application source files are imported by this notebook." ] }, { "cell_type": "code", "execution_count": null, "id": "java-download-dependencies", "metadata": {}, "outputs": [], "source": [ "import java.io.IOException;\n", "import java.nio.file.Files;\n", "import java.nio.file.Path;\n", "\n", "Path dependencyDirectory = Path.of(\"java-notebook-dependencies\");\n", "Path pomFile = dependencyDirectory.resolve(\"pom.xml\");\n", "try {\n", " Files.createDirectories(dependencyDirectory);\n", "\n", "String pom = \"\"\"\n", " \n", " \n", " 4.0.0\n", " local.notebook\n", " langgraph4j-notebook-dependencies\n", " 1.0.0\n", " \n", " \n", " \n", " dev.langchain4j\n", " langchain4j-bom\n", " 1.19.0\n", " pom\n", " import\n", " \n", " \n", " \n", " \n", " \n", " org.bsc.langgraph4j\n", " langgraph4j-agent-executor\n", " 1.8.26\n", " \n", " \n", " org.bsc.langgraph4j\n", " langgraph4j-langchain4j\n", " 1.8.26\n", " \n", " \n", " dev.langchain4j\n", " langchain4j-open-ai\n", " \n", " \n", " \n", " \"\"\";\n", " Files.writeString(pomFile, pom);\n", "\n", " Process maven = new ProcessBuilder(\n", " \"mvn\", \"-f\", pomFile.toString(), \"dependency:copy-dependencies\",\n", " \"-DincludeScope=runtime\", \"-DoutputDirectory=lib\")\n", " .inheritIO()\n", " .start();\n", " int exitCode = maven.waitFor();\n", " if (exitCode != 0) {\n", " throw new IllegalStateException(\"Maven dependency download failed with exit code \" + exitCode);\n", " }\n", " System.out.println(\"Dependencies are ready in \" + dependencyDirectory.resolve(\"lib\").toAbsolutePath());\n", "} catch (IOException | InterruptedException exception) {\n", " if (exception instanceof InterruptedException) {\n", " Thread.currentThread().interrupt();\n", " }\n", " throw new IllegalStateException(\"Could not download Maven dependencies\", exception);\n", "}" ] }, { "cell_type": "code", "execution_count": null, "id": "java-classpath", "metadata": {}, "outputs": [], "source": [ "%classpath java-notebook-dependencies/lib/*" ] }, { "cell_type": "markdown", "id": "java-step-2", "metadata": {}, "source": [ "### **Step 2: Configure the model securely**\n", "\n", "Set `OPENAI_API_KEY` in the environment before starting Jupyter. Do not paste an API key into a notebook cell. The model and endpoint can also be overridden for an OpenAI-compatible provider such as DeepInfra." ] }, { "cell_type": "code", "execution_count": null, "id": "java-config", "metadata": {}, "outputs": [], "source": [ "import dev.langchain4j.agent.tool.P;\n", "import dev.langchain4j.agent.tool.ReturnBehavior;\n", "import dev.langchain4j.agent.tool.Tool;\n", "import dev.langchain4j.data.message.AiMessage;\n", "import dev.langchain4j.data.message.ChatMessage;\n", "import dev.langchain4j.data.message.SystemMessage;\n", "import dev.langchain4j.data.message.ToolExecutionResultMessage;\n", "import dev.langchain4j.data.message.UserMessage;\n", "import dev.langchain4j.model.chat.ChatModel;\n", "import dev.langchain4j.model.openai.OpenAiChatModel;\n", "import org.bsc.langgraph4j.CompiledGraph;\n", "import org.bsc.langgraph4j.GraphDefinition;\n", "import org.bsc.langgraph4j.GraphInput;\n", "import org.bsc.langgraph4j.GraphStateException;\n", "import org.bsc.langgraph4j.RunnableConfig;\n", "import org.bsc.langgraph4j.StateGraph;\n", "import org.bsc.langgraph4j.agentexecutor.AgentExecutor;\n", "import org.bsc.langgraph4j.action.AsyncNodeAction;\n", "import org.bsc.langgraph4j.checkpoint.MemorySaver;\n", "import org.bsc.langgraph4j.langchain4j.serializer.std.LC4jStateSerializer;\n", "import org.bsc.langgraph4j.prebuilt.MessagesState;\n", "import org.bsc.langgraph4j.prebuilt.MessagesStateGraph;\n", "import java.util.Objects;\n", "import java.util.concurrent.CompletableFuture;\n", "import java.util.List;\n", "import java.util.Map;\n", "\n", "String apiKey = System.getenv(\"OPENAI_API_KEY\");\n", "if (apiKey == null || apiKey.isBlank()) {\n", " throw new IllegalStateException(\"Set OPENAI_API_KEY before running the model cells.\");\n", "}\n", "\n", "String modelName = System.getenv().getOrDefault(\"OPENAI_MODEL\", \"gpt-4o-mini\");\n", "String baseUrl = System.getenv().getOrDefault(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\");\n", "System.out.println(\"Using model: \" + modelName);\n", "System.out.println(\"Using endpoint: \" + baseUrl);" ] }, { "cell_type": "markdown", "id": "java-step-3", "metadata": {}, "source": [ "### **Step 3: Define the agent tools**\n", "\n", "The Java implementation expresses tools with LangChain4j's `@Tool` and `@P` annotations. All tool classes are defined in the next cell, so the notebook does not depend on application source files. The advisor-specific wrappers expose only the tools that belong to each agent: `getCareerPaths`, `getLearningResources`, `transfer_to_education_advisor`, and `transfer_to_career_advisor`." ] }, { "cell_type": "code", "execution_count": null, "id": "java-tools", "metadata": {}, "outputs": [], "source": [ "interface Advisor {\n", " List respond(List messages);\n", "}\n", "\n", "class CareerEducationTools {\n", " private static final List CAREER_PATHS =\n", " List.of(\"data science\", \"product management\", \"cybersecurity\");\n", " private static final Map> LEARNING_RESOURCES = Map.of(\n", " \"data science\", List.of(\"Coursera: IBM Data Science\", \"edX: Harvard's Data Science Series\"),\n", " \"product management\", List.of(\"Udemy: Become a Product Manager\", \"Reforge Programs\"),\n", " \"cybersecurity\", List.of(\"Cybrary\", \"CompTIA Security+ Certification\"));\n", "\n", " @Tool(\"Suggest career options based on general user interest.\")\n", " public String getCareerPaths() {\n", " return CAREER_PATHS.get(java.util.concurrent.ThreadLocalRandom.current()\n", " .nextInt(CAREER_PATHS.size()));\n", " }\n", "\n", " @Tool(\"Provide online resources or certifications for a given career path.\")\n", " public List getLearningResources(\n", " @P(\"One of: data science, product management, cybersecurity\") String career) {\n", " List resources = LEARNING_RESOURCES.get(career.toLowerCase());\n", " if (resources == null) {\n", " throw new IllegalArgumentException(\"Unsupported career path: \" + career);\n", " }\n", " return resources;\n", " }\n", "\n", " @Tool(value = \"Ask the education advisor agent for help.\",\n", " returnBehavior = ReturnBehavior.IMMEDIATE)\n", " public String transferToEducationAdvisor() {\n", " return \"Successfully transferred to education advisor.\";\n", " }\n", "\n", " @Tool(value = \"Ask the career advisor agent for help.\",\n", " returnBehavior = ReturnBehavior.IMMEDIATE)\n", " public String transferToCareerAdvisor() {\n", " return \"Successfully transferred to career advisor.\";\n", " }\n", "}\n", "\n", "class CareerAdvisorTools {\n", " private final CareerEducationTools tools;\n", "\n", " CareerAdvisorTools(CareerEducationTools tools) {\n", " this.tools = tools;\n", " }\n", "\n", " @Tool(\"Suggest career options based on general user interest.\")\n", " public String getCareerPaths() {\n", " return tools.getCareerPaths();\n", " }\n", "\n", " @Tool(name = \"transfer_to_education_advisor\",\n", " value = \"Ask the education advisor agent for help.\",\n", " returnBehavior = ReturnBehavior.IMMEDIATE)\n", " public String transferToEducationAdvisor() {\n", " return tools.transferToEducationAdvisor();\n", " }\n", "}\n", "\n", "class EducationAdvisorTools {\n", " private final CareerEducationTools tools;\n", "\n", " EducationAdvisorTools(CareerEducationTools tools) {\n", " this.tools = tools;\n", " }\n", "\n", " @Tool(\"Provide online resources or certifications for a given career path.\")\n", " public List getLearningResources(\n", " @P(\"One of: data science, product management, cybersecurity\") String career) {\n", " return tools.getLearningResources(career);\n", " }\n", "\n", " @Tool(name = \"transfer_to_career_advisor\",\n", " value = \"Ask the career advisor agent for help.\",\n", " returnBehavior = ReturnBehavior.IMMEDIATE)\n", " public String transferToCareerAdvisor() {\n", " return tools.transferToCareerAdvisor();\n", " }\n", "}\n", "\n", "CareerEducationTools toolSet = new CareerEducationTools();\n", "CareerAdvisorTools careerTools = new CareerAdvisorTools(toolSet);\n", "EducationAdvisorTools educationTools = new EducationAdvisorTools(toolSet);\n", "\n", "System.out.println(\"Example career: \" + toolSet.getCareerPaths());\n", "System.out.println(\"Data science resources: \" + toolSet.getLearningResources(\"data science\"));" ] }, { "cell_type": "markdown", "id": "java-step-4", "metadata": {}, "source": [ "### **Step 4: Create the specialized agents**\n", "\n", "Each agent uses the same chat model but has a different system prompt and tool set. `AgentExecutor` provides the ReAct-style tool-calling loop, while the embedded graph controller will decide which advisor receives the next turn." ] }, { "cell_type": "code", "execution_count": null, "id": "java-agents", "metadata": {}, "outputs": [], "source": [ "ChatModel model = OpenAiChatModel.builder()\n", " .apiKey(apiKey)\n", " .modelName(modelName)\n", " .baseUrl(baseUrl)\n", " .temperature(0.0)\n", " .maxRetries(2)\n", " .build();\n", "\n", "final CompiledGraph careerAgent;\n", "final CompiledGraph educationAgent;\n", "try {\n", " careerAgent = AgentExecutor.builder()\n", " .chatModel(model)\n", " .systemMessage(SystemMessage.from(\n", " \"You are a career expert. Help users explore career options. \"\n", " + \"If they ask about courses or education, transfer to the education advisor. \"\n", " + \"Always explain your reasoning before transferring.\"))\n", " .toolsFromObject(careerTools)\n", " .build()\n", " .compile();\n", "\n", " educationAgent = AgentExecutor.builder()\n", " .chatModel(model)\n", " .systemMessage(SystemMessage.from(\n", " \"You are an education expert. Recommend learning paths for specific careers. \"\n", " + \"If the user changes their career preference, transfer back to the career advisor. \"\n", " + \"Always explain your reasoning before transferring.\"))\n", " .toolsFromObject(educationTools)\n", " .build()\n", " .compile();\n", "} catch (GraphStateException exception) {\n", " throw new IllegalStateException(\"Could not compile the advisor agents\", exception);\n", "}" ] }, { "cell_type": "markdown", "id": "java-step-5", "metadata": {}, "source": [ "### **Step 5: Wrap the agents in LangGraph4j advisors**\n", "\n", "The embedded `Advisor` functional interface accepts the complete `ChatMessage` history and returns the messages generated by an agent. This is the Java equivalent of the Python `@task` functions." ] }, { "cell_type": "code", "execution_count": null, "id": "java-advisor-functions", "metadata": {}, "outputs": [], "source": [ "Advisor careerAdvisor = messages -> careerAgent.invoke(Map.of(\"messages\", messages))\n", " .orElseThrow(() -> new IllegalStateException(\"Career advisor produced no state\"))\n", " .messages();\n", "\n", "Advisor educationAdvisor = messages -> educationAgent.invoke(Map.of(\"messages\", messages))\n", " .orElseThrow(() -> new IllegalStateException(\"Education advisor produced no state\"))\n", " .messages();" ] }, { "cell_type": "markdown", "id": "java-step-6", "metadata": {}, "source": [ "### **Step 6: Create the multi-turn controller**\n", "\n", "The embedded `CareerEducationGraph` is the LangGraph4j controller. It starts at the career advisor, stores messages in a `MemorySaver`, interrupts after each answer, and routes the next turn to the education advisor when the user asks about courses, learning, or resources. A request using the same thread ID resumes the checkpointed conversation." ] }, { "cell_type": "code", "execution_count": null, "id": "java-graph", "metadata": {}, "outputs": [], "source": [ "class CareerEducationGraph {\n", " static final String CAREER_ADVISOR = \"career_advisor\";\n", " static final String EDUCATION_ADVISOR = \"education_advisor\";\n", " private static final String CAREER_WAIT = \"career_wait\";\n", " private static final String EDUCATION_WAIT = \"education_wait\";\n", "\n", " private final CompiledGraph> graph;\n", "\n", " CareerEducationGraph(Advisor careerAdvisor, Advisor educationAdvisor) {\n", " Objects.requireNonNull(careerAdvisor, \"careerAdvisor\");\n", " Objects.requireNonNull(educationAdvisor, \"educationAdvisor\");\n", " try {\n", " var serializer = new LC4jStateSerializer>(MessagesState::new);\n", " StateGraph> workflow = new MessagesStateGraph<>(serializer);\n", " workflow.addNode(CAREER_ADVISOR,\n", " AsyncNodeAction.node_async(state -> invoke(careerAdvisor, state)));\n", " workflow.addNode(EDUCATION_ADVISOR,\n", " AsyncNodeAction.node_async(state -> invoke(educationAdvisor, state)));\n", " workflow.addNode(CAREER_WAIT, AsyncNodeAction.node_async(state -> Map.of()));\n", " workflow.addNode(EDUCATION_WAIT, AsyncNodeAction.node_async(state -> Map.of()));\n", " workflow.addEdge(GraphDefinition.START, CAREER_ADVISOR);\n", " workflow.addEdge(CAREER_ADVISOR, CAREER_WAIT);\n", " workflow.addEdge(EDUCATION_ADVISOR, EDUCATION_WAIT);\n", " workflow.addConditionalEdges(CAREER_WAIT,\n", " state -> CompletableFuture.completedFuture(nextAdvisor(state, CAREER_ADVISOR)),\n", " Map.of(CAREER_ADVISOR, CAREER_ADVISOR, EDUCATION_ADVISOR, EDUCATION_ADVISOR));\n", " workflow.addConditionalEdges(EDUCATION_WAIT,\n", " state -> CompletableFuture.completedFuture(nextAdvisor(state, EDUCATION_ADVISOR)),\n", " Map.of(CAREER_ADVISOR, CAREER_ADVISOR, EDUCATION_ADVISOR, EDUCATION_ADVISOR));\n", " graph = workflow.compile(org.bsc.langgraph4j.CompileConfig.builder()\n", " .checkpointSaver(new MemorySaver())\n", " .interruptAfter(CAREER_WAIT, EDUCATION_WAIT)\n", " .interruptBeforeEdge(true)\n", " .releaseThread(false)\n", " .build());\n", " } catch (GraphStateException exception) {\n", " throw new IllegalStateException(\"Could not compile the career conversation graph\", exception);\n", " }\n", " }\n", "\n", " ConversationTurn turn(String threadId, String userInput) {\n", " if (threadId == null || threadId.isBlank()) {\n", " throw new IllegalArgumentException(\"threadId must not be blank\");\n", " }\n", " if (userInput == null || userInput.isBlank()) {\n", " throw new IllegalArgumentException(\"userInput must not be blank\");\n", " }\n", " var config = RunnableConfig.builder().threadId(threadId).build();\n", " Map input = Map.of(MessagesState.MESSAGES_STATE,\n", " List.of(UserMessage.from(userInput)));\n", " GraphInput graphInput = graph.stateOf(config).isPresent()\n", " ? GraphInput.resume(input) : GraphInput.args(input);\n", " var outputs = graph.stream(graphInput, config).stream().toList();\n", " if (outputs.isEmpty()) {\n", " throw new IllegalStateException(\"The conversation graph produced no output\");\n", " }\n", " var output = outputs.get(outputs.size() - 1);\n", " String response = output.state().messages().stream()\n", " .filter(AiMessage.class::isInstance).map(AiMessage.class::cast)\n", " .reduce((first, second) -> second).map(AiMessage::text).orElse(\"\");\n", " String advisor = CAREER_WAIT.equals(output.node()) ? CAREER_ADVISOR\n", " : EDUCATION_WAIT.equals(output.node()) ? EDUCATION_ADVISOR : output.node();\n", " return new ConversationTurn(threadId, advisor, response, true);\n", " }\n", "\n", " private static Map invoke(Advisor advisor, MessagesState state) {\n", " int previousSize = state.messages().size();\n", " List generated = advisor.respond(List.copyOf(state.messages()));\n", " if (generated == null || generated.isEmpty()) {\n", " throw new IllegalStateException(\"Advisor produced no messages\");\n", " }\n", " List newMessages = generated.size() >= previousSize\n", " && generated.subList(0, previousSize).equals(state.messages())\n", " ? generated.subList(previousSize, generated.size()) : generated;\n", " return Map.of(MessagesState.MESSAGES_STATE, List.copyOf(newMessages));\n", " }\n", "\n", " private static String nextAdvisor(MessagesState state, String currentAdvisor) {\n", " for (int index = state.messages().size() - 1; index >= 0; index--) {\n", " var message = state.messages().get(index);\n", " if (message instanceof ToolExecutionResultMessage toolResult) {\n", " if (\"transfer_to_education_advisor\".equals(toolResult.toolName())) {\n", " return EDUCATION_ADVISOR;\n", " }\n", " if (\"transfer_to_career_advisor\".equals(toolResult.toolName())) {\n", " return CAREER_ADVISOR;\n", " }\n", " }\n", " }\n", " String latestUserText = state.messages().stream()\n", " .filter(UserMessage.class::isInstance).map(UserMessage.class::cast)\n", " .reduce((first, second) -> second).map(UserMessage::singleText)\n", " .orElse(\"\").toLowerCase();\n", " if (CAREER_ADVISOR.equals(currentAdvisor)\n", " && containsAny(latestUserText, \"course\", \"education\", \"learn\", \"resource\")) {\n", " return EDUCATION_ADVISOR;\n", " }\n", " if (EDUCATION_ADVISOR.equals(currentAdvisor)\n", " && containsAny(latestUserText, \"career\", \"change\", \"different path\")) {\n", " return CAREER_ADVISOR;\n", " }\n", " return currentAdvisor;\n", " }\n", "\n", " private static boolean containsAny(String text, String... terms) {\n", " for (String term : terms) {\n", " if (text.contains(term)) return true;\n", " }\n", " return false;\n", " }\n", "\n", " record ConversationTurn(String threadId, String advisor, String response,\n", " boolean waitingForUser) {}\n", "}\n", "\n", "CareerEducationGraph conversation = new CareerEducationGraph(careerAdvisor, educationAdvisor);\n", "String threadId = java.util.UUID.randomUUID().toString();\n", "System.out.println(\"Conversation thread: \" + threadId);" ] }, { "cell_type": "markdown", "id": "java-step-7", "metadata": {}, "source": [ "### **Step 7: Test the multi-turn conversation**\n", "\n", "The three prompts below use one stable thread ID. The first turn starts with the career advisor; the second asks about courses and is routed to the education advisor; the third remains in the education conversation. This is the Java equivalent of resuming the Python graph with `Command(resume=...)`." ] }, { "cell_type": "code", "execution_count": null, "id": "java-conversation", "metadata": {}, "outputs": [], "source": [ "List prompts = List.of(\n", " \"I'm interested in technology but not sure what career fits me.\",\n", " \"That sounds good. What courses should I take to get started?\",\n", " \"Awesome! Are these resources beginner friendly?\");\n", "\n", "for (int index = 0; index < prompts.size(); index++) {\n", " String prompt = prompts.get(index);\n", " var turn = conversation.turn(threadId, prompt);\n", " System.out.println(\"\\n--- Conversation Turn \" + (index + 1) + \" ---\");\n", " System.out.println(\"User: \" + prompt);\n", " System.out.println(\"Advisor: \" + turn.advisor());\n", " System.out.println(\"Assistant: \" + turn.response());\n", "}" ] }, { "cell_type": "markdown", "id": "java-notes", "metadata": {}, "source": [ "### Notes\n", "\n", "`MemorySaver` is process-local and is intended for this tutorial. For a deployed application, replace it with a persistent checkpoint saver and keep the thread ID stable across requests. The same graph is also available through the Java service endpoint documented in `java/README.md`." ] }, { "cell_type": "raw", "id": "99028f88-688f-41f6-9112-e59612efb75b", "metadata": {}, "source": [] } ], "metadata": { "kernelspec": { "display_name": "Java (jjava)", "language": "java", "name": "java" }, "language_info": { "codemirror_mode": "java", "file_extension": ".jshell", "mimetype": "text/x-java-source", "name": "Java", "pygments_lexer": "java", "version": "21.0.10+8-LTS-217" } }, "nbformat": 4, "nbformat_minor": 5 }