mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-20 06:35:46 +02:00
java langgraph-checkpoint
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
##############################
|
||||
## Java
|
||||
##############################
|
||||
.mtj.tmp/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
##############################
|
||||
## Maven
|
||||
##############################
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
pom.xml.bak
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
##############################
|
||||
## Gradle
|
||||
##############################
|
||||
bin/
|
||||
build/
|
||||
.gradle
|
||||
.gradletasknamecache
|
||||
gradle-app.setting
|
||||
!gradle-wrapper.jar
|
||||
|
||||
##############################
|
||||
## IntelliJ
|
||||
##############################
|
||||
out/
|
||||
.idea/
|
||||
.idea_modules/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
##############################
|
||||
## Eclipse
|
||||
##############################
|
||||
.settings/
|
||||
bin/
|
||||
tmp/
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~.nib
|
||||
local.properties
|
||||
.loadpath
|
||||
.factorypath
|
||||
|
||||
##############################
|
||||
## NetBeans
|
||||
##############################
|
||||
nbproject/private/
|
||||
build/
|
||||
nbbuild/
|
||||
dist/
|
||||
nbdist/
|
||||
nbactions.xml
|
||||
nb-configuration.xml
|
||||
|
||||
##############################
|
||||
## Visual Studio Code
|
||||
##############################
|
||||
.vscode/
|
||||
.code-workspace
|
||||
|
||||
##############################
|
||||
## OS X
|
||||
##############################
|
||||
.DS_Store
|
||||
|
||||
##############################
|
||||
## Miscellaneous
|
||||
##############################
|
||||
*.log
|
||||
@@ -0,0 +1,63 @@
|
||||
# LangGraph Java
|
||||
|
||||
A Java port of LangGraph, a framework for building stateful, observable applications with large language models (LLMs).
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `langgraph-checkpoint`: Base persistence interfaces
|
||||
- `langgraph-core`: Main library with channels, Pregel, and StateGraph
|
||||
- `langgraph-examples`: Example applications
|
||||
|
||||
## Requirements
|
||||
|
||||
- Java 17 or higher
|
||||
- Gradle 7.0 or higher
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Graph-based architecture with nodes and edges
|
||||
- Type-safe state schema using Java Records
|
||||
- Cyclical execution patterns
|
||||
- Checkpoint integration for persistence
|
||||
- Streaming support
|
||||
- Human-in-the-loop capabilities
|
||||
|
||||
## Usage Example
|
||||
|
||||
```java
|
||||
// Create a graph with our schema
|
||||
StateGraph<CounterState> graph = new StateGraph<>(CounterState.class);
|
||||
|
||||
// Add nodes
|
||||
graph.addNode("increment", state -> {
|
||||
Map<String, Object> updates = new HashMap<>();
|
||||
updates.put("count", state.count() + 1);
|
||||
return updates;
|
||||
});
|
||||
|
||||
// Add edges
|
||||
graph.addEdge(START, "increment");
|
||||
graph.addEdge("increment", "check");
|
||||
|
||||
// Add conditional edge
|
||||
graph.addConditionalEdges("check", state -> {
|
||||
if (state.count() >= 3) {
|
||||
return "finish";
|
||||
}
|
||||
return "increment";
|
||||
});
|
||||
|
||||
// Compile and run
|
||||
CompiledStateGraph<CounterState> compiled = graph.compile();
|
||||
CounterState result = compiled.invoke(new CounterState(0, ""));
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
@@ -0,0 +1,39 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group = 'com.langgraph'
|
||||
version = '0.1.0-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java-library'
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs << '-parameters'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Testing dependencies
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
|
||||
testImplementation 'org.mockito:mockito-core:5.2.0'
|
||||
testImplementation 'org.assertj:assertj-core:3.24.2'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies {
|
||||
// MessagePack for serialization
|
||||
implementation 'org.msgpack:msgpack-core:0.9.3'
|
||||
implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.3'
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.langgraph.checkpoint.base;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Asynchronous interface for saving and loading checkpoints.
|
||||
*/
|
||||
public interface AsyncBaseCheckpointSaver {
|
||||
/**
|
||||
* Create a new checkpoint asynchronously.
|
||||
*
|
||||
* @param threadId The ID of the thread to checkpoint
|
||||
* @param channelValues The values of the channels to checkpoint
|
||||
* @return CompletableFuture with the ID of the new checkpoint
|
||||
*/
|
||||
CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues);
|
||||
|
||||
/**
|
||||
* Get values from a checkpoint asynchronously.
|
||||
*
|
||||
* @param checkpointId The ID of the checkpoint to load
|
||||
* @return CompletableFuture with the channel values from the checkpoint, or empty if not found
|
||||
*/
|
||||
CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId);
|
||||
|
||||
/**
|
||||
* List all checkpoints for a thread asynchronously.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
* @return CompletableFuture with list of checkpoint IDs
|
||||
*/
|
||||
CompletableFuture<List<String>> listAsync(String threadId);
|
||||
|
||||
/**
|
||||
* Get the latest checkpoint for a thread asynchronously.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
* @return CompletableFuture with the ID of the latest checkpoint, or empty if none exists
|
||||
*/
|
||||
CompletableFuture<Optional<String>> latestAsync(String threadId);
|
||||
|
||||
/**
|
||||
* Delete a checkpoint asynchronously.
|
||||
*
|
||||
* @param checkpointId The ID of the checkpoint to delete
|
||||
* @return CompletableFuture completed when deletion is done
|
||||
*/
|
||||
CompletableFuture<Void> deleteAsync(String checkpointId);
|
||||
|
||||
/**
|
||||
* Clear all checkpoints for a thread asynchronously.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
* @return CompletableFuture completed when clearing is done
|
||||
*/
|
||||
CompletableFuture<Void> clearAsync(String threadId);
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.langgraph.checkpoint.base;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Interface for saving and loading checkpoints.
|
||||
*/
|
||||
public interface BaseCheckpointSaver {
|
||||
/**
|
||||
* Create a new checkpoint.
|
||||
*
|
||||
* @param threadId The ID of the thread to checkpoint
|
||||
* @param channelValues The values of the channels to checkpoint
|
||||
* @return The ID of the new checkpoint
|
||||
*/
|
||||
String checkpoint(String threadId, Map<String, Object> channelValues);
|
||||
|
||||
/**
|
||||
* Get values from a checkpoint.
|
||||
*
|
||||
* @param checkpointId The ID of the checkpoint to load
|
||||
* @return The channel values from the checkpoint, or empty if not found
|
||||
*/
|
||||
Optional<Map<String, Object>> getValues(String checkpointId);
|
||||
|
||||
/**
|
||||
* List all checkpoints for a thread.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
* @return List of checkpoint IDs
|
||||
*/
|
||||
List<String> list(String threadId);
|
||||
|
||||
/**
|
||||
* Get the latest checkpoint for a thread.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
* @return The ID of the latest checkpoint, or empty if none exists
|
||||
*/
|
||||
Optional<String> latest(String threadId);
|
||||
|
||||
/**
|
||||
* Delete a checkpoint.
|
||||
*
|
||||
* @param checkpointId The ID of the checkpoint to delete
|
||||
*/
|
||||
void delete(String checkpointId);
|
||||
|
||||
/**
|
||||
* Clear all checkpoints for a thread.
|
||||
*
|
||||
* @param threadId The ID of the thread
|
||||
*/
|
||||
void clear(String threadId);
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.langgraph.checkpoint.base;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Utility class for generating deterministic IDs.
|
||||
*/
|
||||
public final class ID {
|
||||
private ID() {
|
||||
// Prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a deterministic UUID based on a namespace and name.
|
||||
*
|
||||
* @param namespace The namespace for the ID
|
||||
* @param name The name within the namespace
|
||||
* @return A UUID
|
||||
*/
|
||||
public static UUID uuid(String namespace, String name) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
md.update(namespace.getBytes(StandardCharsets.UTF_8));
|
||||
md.update(name.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] digest = md.digest();
|
||||
|
||||
// Set the version (4) and variant (RFC4122) bits
|
||||
digest[6] = (byte) ((digest[6] & 0x0F) | 0x40);
|
||||
digest[8] = (byte) ((digest[8] & 0x3F) | 0x80);
|
||||
|
||||
long msb = 0;
|
||||
long lsb = 0;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
msb = (msb << 8) | (digest[i] & 0xff);
|
||||
}
|
||||
|
||||
for (int i = 8; i < 16; i++) {
|
||||
lsb = (lsb << 8) | (digest[i] & 0xff);
|
||||
}
|
||||
|
||||
return new UUID(msb, lsb);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-1 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a checkpoint ID.
|
||||
*
|
||||
* @param threadId The thread ID
|
||||
* @return A checkpoint ID
|
||||
*/
|
||||
public static String checkpointId(String threadId) {
|
||||
return uuid("checkpoint", threadId + "/" + System.currentTimeMillis()).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL-safe base64 encoded ID.
|
||||
*
|
||||
* @param namespace The namespace for the ID
|
||||
* @param name The name within the namespace
|
||||
* @return A URL-safe base64-encoded ID
|
||||
*/
|
||||
public static String urlSafeId(String namespace, String name) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
md.update(namespace.getBytes(StandardCharsets.UTF_8));
|
||||
md.update(name.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] digest = md.digest();
|
||||
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package com.langgraph.checkpoint.base.memory;
|
||||
|
||||
import com.langgraph.checkpoint.base.AsyncBaseCheckpointSaver;
|
||||
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Asynchronous in-memory implementation of a checkpoint saver.
|
||||
* This is a thin wrapper around the synchronous implementation that
|
||||
* executes operations asynchronously.
|
||||
*/
|
||||
public class AsyncMemoryCheckpointSaver implements AsyncBaseCheckpointSaver {
|
||||
private final BaseCheckpointSaver synchronousSaver;
|
||||
|
||||
/**
|
||||
* Create an async memory checkpoint saver.
|
||||
*/
|
||||
public AsyncMemoryCheckpointSaver() {
|
||||
this.synchronousSaver = new MemoryCheckpointSaver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an async memory checkpoint saver with an existing synchronous saver.
|
||||
*
|
||||
* @param synchronousSaver The synchronous checkpoint saver to wrap
|
||||
*/
|
||||
public AsyncMemoryCheckpointSaver(BaseCheckpointSaver synchronousSaver) {
|
||||
this.synchronousSaver = synchronousSaver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> checkpointAsync(String threadId, Map<String, Object> channelValues) {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
synchronousSaver.checkpoint(threadId, channelValues));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<Map<String, Object>>> getValuesAsync(String checkpointId) {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
synchronousSaver.getValues(checkpointId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<String>> listAsync(String threadId) {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
synchronousSaver.list(threadId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<String>> latestAsync(String threadId) {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
synchronousSaver.latest(threadId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> deleteAsync(String checkpointId) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
synchronousSaver.delete(checkpointId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> clearAsync(String threadId) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
synchronousSaver.clear(threadId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying synchronous checkpoint saver.
|
||||
*
|
||||
* @return The synchronous checkpoint saver
|
||||
*/
|
||||
public BaseCheckpointSaver getSynchronousSaver() {
|
||||
return synchronousSaver;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.langgraph.checkpoint.base.memory;
|
||||
|
||||
import com.langgraph.checkpoint.base.BaseCheckpointSaver;
|
||||
import com.langgraph.checkpoint.base.ID;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* In-memory implementation of a checkpoint saver.
|
||||
*/
|
||||
public class MemoryCheckpointSaver implements BaseCheckpointSaver {
|
||||
private final Map<String, Map<String, Object>> checkpoints = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<String>> threadCheckpoints = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public String checkpoint(String threadId, Map<String, Object> channelValues) {
|
||||
String checkpointId = ID.checkpointId(threadId);
|
||||
|
||||
// Store the checkpoint
|
||||
checkpoints.put(checkpointId, new HashMap<>(channelValues));
|
||||
|
||||
// Add to thread's checkpoints
|
||||
threadCheckpoints.computeIfAbsent(threadId, k ->
|
||||
Collections.synchronizedList(new ArrayList<>())).add(checkpointId);
|
||||
|
||||
return checkpointId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Map<String, Object>> getValues(String checkpointId) {
|
||||
Map<String, Object> values = checkpoints.get(checkpointId);
|
||||
return Optional.ofNullable(values).map(HashMap::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> list(String threadId) {
|
||||
List<String> result = threadCheckpoints.get(threadId);
|
||||
return result != null ? new ArrayList<>(result) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> latest(String threadId) {
|
||||
List<String> checkpoints = threadCheckpoints.get(threadId);
|
||||
|
||||
if (checkpoints == null || checkpoints.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(checkpoints.get(checkpoints.size() - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String checkpointId) {
|
||||
// Remove the checkpoint
|
||||
Map<String, Object> removed = checkpoints.remove(checkpointId);
|
||||
|
||||
if (removed != null) {
|
||||
// Find and remove from thread's checkpoints
|
||||
for (List<String> checkpointsList : threadCheckpoints.values()) {
|
||||
checkpointsList.remove(checkpointId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear(String threadId) {
|
||||
List<String> checkpointIds = threadCheckpoints.remove(threadId);
|
||||
|
||||
if (checkpointIds != null) {
|
||||
// Remove all checkpoints for this thread
|
||||
for (String checkpointId : checkpointIds) {
|
||||
checkpoints.remove(checkpointId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+548
@@ -0,0 +1,548 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
import org.msgpack.core.MessageBufferPacker;
|
||||
import org.msgpack.core.MessagePack;
|
||||
import org.msgpack.core.MessageUnpacker;
|
||||
import org.msgpack.core.MessageFormat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.*;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* MessagePack-based serializer that uses reflection to handle arbitrary Java objects.
|
||||
* Supports primitive types, collections, maps, records, and custom objects.
|
||||
*/
|
||||
public class MsgPackSerializer implements ReflectionSerializer {
|
||||
private final Map<Class<?>, TypeSerializer<?>> serializers = new ConcurrentHashMap<>();
|
||||
private final Map<Class<?>, TypeDeserializer<?>> deserializers = new ConcurrentHashMap<>();
|
||||
private final Map<Class<?>, RecordInfo> recordInfoCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Record component information cache to avoid repeated reflection.
|
||||
*/
|
||||
private static class RecordInfo {
|
||||
final RecordComponent[] components;
|
||||
final Constructor<?> constructor;
|
||||
|
||||
RecordInfo(RecordComponent[] components, Constructor<?> constructor) {
|
||||
this.components = components;
|
||||
this.constructor = constructor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register built-in serializers for common types.
|
||||
*/
|
||||
public MsgPackSerializer() {
|
||||
registerBuiltinTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register built-in serializers for common types.
|
||||
*/
|
||||
private void registerBuiltinTypes() {
|
||||
// UUID serializer
|
||||
registerSerializer(UUID.class, (uuid) -> uuid.toString());
|
||||
registerDeserializer(UUID.class, (str) -> UUID.fromString((String) str));
|
||||
|
||||
// Date serializer
|
||||
registerSerializer(java.util.Date.class, (date) -> date.getTime());
|
||||
registerDeserializer(java.util.Date.class, (millis) -> new Date((Long) millis));
|
||||
|
||||
// Java 8 Date/Time API
|
||||
registerSerializer(Instant.class, (instant) -> instant.toString());
|
||||
registerDeserializer(Instant.class, (str) -> Instant.parse((String) str));
|
||||
|
||||
registerSerializer(LocalDate.class, (date) -> date.toString());
|
||||
registerDeserializer(LocalDate.class, (str) -> LocalDate.parse((String) str));
|
||||
|
||||
registerSerializer(LocalTime.class, (time) -> time.toString());
|
||||
registerDeserializer(LocalTime.class, (str) -> LocalTime.parse((String) str));
|
||||
|
||||
registerSerializer(LocalDateTime.class, (dateTime) -> dateTime.toString());
|
||||
registerDeserializer(LocalDateTime.class, (str) -> LocalDateTime.parse((String) str));
|
||||
|
||||
// Add more built-in serializers as needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer) {
|
||||
serializers.put(type, serializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer) {
|
||||
deserializers.put(type, deserializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(Object obj) {
|
||||
try {
|
||||
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
|
||||
serializeObject(obj, packer);
|
||||
return packer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to serialize object", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object deserialize(byte[] data) {
|
||||
try {
|
||||
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data);
|
||||
return deserializeObject(unpacker);
|
||||
} catch (IOException e) {
|
||||
throw new SerializationException("Failed to deserialize object", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an object to the MessagePack packer.
|
||||
*
|
||||
* @param obj Object to serialize
|
||||
* @param packer MessagePack packer
|
||||
* @throws IOException If packing fails
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void serializeObject(Object obj, MessageBufferPacker packer) throws IOException {
|
||||
if (obj == null) {
|
||||
packer.packNil();
|
||||
return;
|
||||
}
|
||||
|
||||
Class<?> type = obj.getClass();
|
||||
|
||||
// Check for registered serializer
|
||||
if (serializers.containsKey(type)) {
|
||||
TypeSerializer<Object> serializer = (TypeSerializer<Object>) serializers.get(type);
|
||||
Object serialized = serializer.toSerializable(obj);
|
||||
|
||||
// Pack as a special type
|
||||
packer.packMapHeader(2);
|
||||
packer.packString("__type__");
|
||||
packer.packString(type.getName());
|
||||
packer.packString("value");
|
||||
serializeObject(serialized, packer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle primitive types and common objects directly
|
||||
if (obj instanceof String) {
|
||||
packer.packString((String) obj);
|
||||
} else if (obj instanceof Integer) {
|
||||
packer.packInt((Integer) obj);
|
||||
} else if (obj instanceof Long) {
|
||||
packer.packLong((Long) obj);
|
||||
} else if (obj instanceof Double) {
|
||||
packer.packDouble((Double) obj);
|
||||
} else if (obj instanceof Float) {
|
||||
packer.packFloat((Float) obj);
|
||||
} else if (obj instanceof Boolean) {
|
||||
packer.packBoolean((Boolean) obj);
|
||||
} else if (obj instanceof byte[]) {
|
||||
packer.packBinaryHeader(((byte[]) obj).length);
|
||||
packer.writePayload((byte[]) obj);
|
||||
} else if (obj instanceof List) {
|
||||
List<?> list = (List<?>) obj;
|
||||
packer.packArrayHeader(list.size());
|
||||
for (Object item : list) {
|
||||
serializeObject(item, packer);
|
||||
}
|
||||
} else if (obj instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
packer.packMapHeader(map.size());
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
serializeObject(entry.getKey(), packer);
|
||||
serializeObject(entry.getValue(), packer);
|
||||
}
|
||||
} else if (obj instanceof Enum<?>) {
|
||||
// Handle enums by name
|
||||
packer.packMapHeader(2);
|
||||
packer.packString("__type__");
|
||||
packer.packString(type.getName());
|
||||
packer.packString("value");
|
||||
packer.packString(((Enum<?>) obj).name());
|
||||
} else if (type.isRecord()) {
|
||||
// Handle Record types
|
||||
serializeRecord(obj, packer);
|
||||
} else {
|
||||
// Handle custom objects with reflection
|
||||
serializeCustomObject(obj, packer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a Record object.
|
||||
*
|
||||
* @param record The record to serialize
|
||||
* @param packer The MessagePack packer
|
||||
* @throws IOException If packing fails
|
||||
*/
|
||||
private void serializeRecord(Object record, MessageBufferPacker packer) throws IOException {
|
||||
Class<?> recordClass = record.getClass();
|
||||
|
||||
// Pack as a special type with fields
|
||||
packer.packMapHeader(2);
|
||||
packer.packString("__type__");
|
||||
packer.packString(recordClass.getName());
|
||||
packer.packString("fields");
|
||||
|
||||
RecordComponent[] components = recordClass.getRecordComponents();
|
||||
packer.packMapHeader(components.length);
|
||||
|
||||
for (RecordComponent component : components) {
|
||||
packer.packString(component.getName());
|
||||
try {
|
||||
Method accessor = component.getAccessor();
|
||||
Object value = accessor.invoke(record);
|
||||
serializeObject(value, packer);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new SerializationException("Failed to access record component: " + component.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a custom object using reflection.
|
||||
*
|
||||
* @param obj The object to serialize
|
||||
* @param packer The MessagePack packer
|
||||
* @throws IOException If packing fails
|
||||
*/
|
||||
private void serializeCustomObject(Object obj, MessageBufferPacker packer) throws IOException {
|
||||
Class<?> objClass = obj.getClass();
|
||||
|
||||
// Pack as a special type with fields
|
||||
packer.packMapHeader(2);
|
||||
packer.packString("__type__");
|
||||
packer.packString(objClass.getName());
|
||||
packer.packString("fields");
|
||||
|
||||
// Get all fields including inherited ones
|
||||
List<Field> fields = getAllFields(objClass);
|
||||
|
||||
// Filter out transient fields
|
||||
List<Field> serializableFields = fields.stream()
|
||||
.filter(field -> !Modifier.isTransient(field.getModifiers()) &&
|
||||
!Modifier.isStatic(field.getModifiers()))
|
||||
.toList();
|
||||
|
||||
packer.packMapHeader(serializableFields.size());
|
||||
|
||||
for (Field field : serializableFields) {
|
||||
packer.packString(field.getName());
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(obj);
|
||||
serializeObject(value, packer);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new SerializationException("Failed to access field: " + field.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all fields for a class including inherited fields.
|
||||
*
|
||||
* @param clazz The class to get fields for
|
||||
* @return List of all fields
|
||||
*/
|
||||
private List<Field> getAllFields(Class<?> clazz) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
Class<?> currentClass = clazz;
|
||||
|
||||
while (currentClass != null && currentClass != Object.class) {
|
||||
fields.addAll(Arrays.asList(currentClass.getDeclaredFields()));
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an object from the MessagePack unpacker.
|
||||
*
|
||||
* @param unpacker MessagePack unpacker
|
||||
* @return Deserialized object
|
||||
* @throws IOException If unpacking fails
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object deserializeObject(MessageUnpacker unpacker) throws IOException {
|
||||
if (!unpacker.hasNext()) {
|
||||
throw new SerializationException("Unexpected end of data");
|
||||
}
|
||||
|
||||
if (unpacker.tryUnpackNil()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
MessageFormat format = unpacker.getNextFormat();
|
||||
|
||||
if (format == MessageFormat.STR8 ||
|
||||
format == MessageFormat.STR16 ||
|
||||
format == MessageFormat.STR32 ||
|
||||
format == MessageFormat.FIXSTR) {
|
||||
return unpacker.unpackString();
|
||||
} else if (format == MessageFormat.INT8 ||
|
||||
format == MessageFormat.INT16 ||
|
||||
format == MessageFormat.INT32 ||
|
||||
format == MessageFormat.INT64 ||
|
||||
format == MessageFormat.UINT8 ||
|
||||
format == MessageFormat.UINT16 ||
|
||||
format == MessageFormat.UINT32 ||
|
||||
format == MessageFormat.UINT64 ||
|
||||
format == MessageFormat.POSFIXINT ||
|
||||
format == MessageFormat.NEGFIXINT) {
|
||||
if (format == MessageFormat.INT64 || format == MessageFormat.UINT64) {
|
||||
return unpacker.unpackLong();
|
||||
} else {
|
||||
try {
|
||||
return unpacker.unpackInt();
|
||||
} catch (Exception e) {
|
||||
// Fallback to long if int unpacking fails
|
||||
return unpacker.unpackLong();
|
||||
}
|
||||
}
|
||||
} else if (format == MessageFormat.FLOAT32 ||
|
||||
format == MessageFormat.FLOAT64) {
|
||||
return unpacker.unpackDouble();
|
||||
} else if (format == MessageFormat.BOOLEAN) {
|
||||
return unpacker.unpackBoolean();
|
||||
} else if (format == MessageFormat.BIN8 ||
|
||||
format == MessageFormat.BIN16 ||
|
||||
format == MessageFormat.BIN32) {
|
||||
int binaryLength = unpacker.unpackBinaryHeader();
|
||||
byte[] binary = new byte[binaryLength];
|
||||
unpacker.readPayload(binary);
|
||||
return binary;
|
||||
} else if (format == MessageFormat.ARRAY16 ||
|
||||
format == MessageFormat.ARRAY32 ||
|
||||
format == MessageFormat.FIXARRAY) {
|
||||
int arraySize = unpacker.unpackArrayHeader();
|
||||
List<Object> list = new ArrayList<>(arraySize);
|
||||
for (int i = 0; i < arraySize; i++) {
|
||||
list.add(deserializeObject(unpacker));
|
||||
}
|
||||
return list;
|
||||
} else if (format == MessageFormat.MAP16 ||
|
||||
format == MessageFormat.MAP32 ||
|
||||
format == MessageFormat.FIXMAP) {
|
||||
int mapSize = unpacker.unpackMapHeader();
|
||||
|
||||
// Handle empty map
|
||||
if (mapSize == 0) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
// Check for special type marker
|
||||
Object firstKey = deserializeObject(unpacker);
|
||||
if (mapSize == 2 && firstKey instanceof String && "__type__".equals(firstKey)) {
|
||||
String typeName = (String) deserializeObject(unpacker);
|
||||
|
||||
// Get the second key
|
||||
Object secondKey = deserializeObject(unpacker);
|
||||
|
||||
if (secondKey instanceof String) {
|
||||
String secondKeyStr = (String) secondKey;
|
||||
|
||||
try {
|
||||
Class<?> type = Class.forName(typeName);
|
||||
|
||||
// Check for registered deserializer
|
||||
if ("value".equals(secondKeyStr) && deserializers.containsKey(type)) {
|
||||
Object serialized = deserializeObject(unpacker);
|
||||
TypeDeserializer<Object> deserializer =
|
||||
(TypeDeserializer<Object>) deserializers.get(type);
|
||||
return deserializer.fromSerialized(serialized);
|
||||
}
|
||||
|
||||
// Handle enums
|
||||
if ("value".equals(secondKeyStr) && type.isEnum()) {
|
||||
String enumValue = (String) deserializeObject(unpacker);
|
||||
return Enum.valueOf((Class<Enum>) type, enumValue);
|
||||
}
|
||||
|
||||
// Handle records
|
||||
if ("fields".equals(secondKeyStr) && type.isRecord()) {
|
||||
return deserializeRecord(type, unpacker);
|
||||
}
|
||||
|
||||
// Handle custom objects
|
||||
if ("fields".equals(secondKeyStr)) {
|
||||
return deserializeCustomObject(type, unpacker);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
// If class not found, fall back to regular map deserialization
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new SerializationException("Failed to deserialize object of type " + typeName, e);
|
||||
}
|
||||
|
||||
// If special type handling failed, read the value to keep unpacker consistent
|
||||
Object secondValue = deserializeObject(unpacker);
|
||||
|
||||
// Create a fallback map with the special type info
|
||||
Map<Object, Object> fallbackMap = new HashMap<>();
|
||||
fallbackMap.put(firstKey, typeName);
|
||||
fallbackMap.put(secondKey, secondValue);
|
||||
return fallbackMap;
|
||||
}
|
||||
|
||||
// If the second key wasn't a string as expected, we need to handle it as a regular map
|
||||
Object firstValue = deserializeObject(unpacker);
|
||||
|
||||
// Create a map with the first key-value pair
|
||||
Map<Object, Object> map = new HashMap<>(mapSize);
|
||||
map.put(firstKey, firstValue);
|
||||
|
||||
// Read the remaining entries
|
||||
for (int i = 1; i < mapSize; i++) {
|
||||
Object key = deserializeObject(unpacker);
|
||||
Object value = deserializeObject(unpacker);
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
return map;
|
||||
} else {
|
||||
// Regular map - we already read the first key
|
||||
Map<Object, Object> map = new HashMap<>(mapSize);
|
||||
|
||||
// Read the first value
|
||||
Object firstValue = deserializeObject(unpacker);
|
||||
map.put(firstKey, firstValue);
|
||||
|
||||
// Read the remaining entries
|
||||
for (int i = 1; i < mapSize; i++) {
|
||||
Object key = deserializeObject(unpacker);
|
||||
Object value = deserializeObject(unpacker);
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
// Default case
|
||||
throw new SerializationException("Unsupported MessagePack format: " + format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a record.
|
||||
*
|
||||
* @param recordClass The record class
|
||||
* @param unpacker The unpacker containing the fields map
|
||||
* @return The deserialized record
|
||||
* @throws IOException If unpacking fails
|
||||
* @throws ReflectiveOperationException If reflection operations fail
|
||||
*/
|
||||
private Object deserializeRecord(Class<?> recordClass, MessageUnpacker unpacker)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
|
||||
// Get record info from cache or create it
|
||||
RecordInfo recordInfo = recordInfoCache.computeIfAbsent(recordClass, cls -> {
|
||||
try {
|
||||
RecordComponent[] components = cls.getRecordComponents();
|
||||
Class<?>[] paramTypes = Arrays.stream(components)
|
||||
.map(RecordComponent::getType)
|
||||
.toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = cls.getDeclaredConstructor(paramTypes);
|
||||
constructor.setAccessible(true);
|
||||
return new RecordInfo(components, constructor);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new SerializationException("Failed to get constructor for record: " + cls.getName(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// Read the fields map
|
||||
int fieldCount = unpacker.unpackMapHeader();
|
||||
Map<String, Object> fieldValues = new HashMap<>(fieldCount);
|
||||
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
String fieldName = (String) deserializeObject(unpacker);
|
||||
Object fieldValue = deserializeObject(unpacker);
|
||||
fieldValues.put(fieldName, fieldValue);
|
||||
}
|
||||
|
||||
// Prepare constructor arguments in the correct order
|
||||
Object[] constructorArgs = new Object[recordInfo.components.length];
|
||||
for (int i = 0; i < recordInfo.components.length; i++) {
|
||||
RecordComponent component = recordInfo.components[i];
|
||||
Object value = fieldValues.get(component.getName());
|
||||
constructorArgs[i] = value;
|
||||
}
|
||||
|
||||
// Create the record instance
|
||||
return recordInfo.constructor.newInstance(constructorArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a custom object.
|
||||
*
|
||||
* @param objectClass The object class
|
||||
* @param unpacker The unpacker containing the fields map
|
||||
* @return The deserialized object
|
||||
* @throws IOException If unpacking fails
|
||||
* @throws ReflectiveOperationException If reflection operations fail
|
||||
*/
|
||||
private Object deserializeCustomObject(Class<?> objectClass, MessageUnpacker unpacker)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
|
||||
// Create instance using default constructor
|
||||
Constructor<?> constructor;
|
||||
try {
|
||||
constructor = objectClass.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new SerializationException(
|
||||
"Class " + objectClass.getName() + " must have a no-arg constructor for deserialization", e);
|
||||
}
|
||||
|
||||
Object instance = constructor.newInstance();
|
||||
|
||||
// Read the fields map
|
||||
int fieldCount = unpacker.unpackMapHeader();
|
||||
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
String fieldName = (String) deserializeObject(unpacker);
|
||||
Object fieldValue = deserializeObject(unpacker);
|
||||
|
||||
try {
|
||||
// Find the field (including in superclasses)
|
||||
Field field = findField(objectClass, fieldName);
|
||||
if (field != null) {
|
||||
field.setAccessible(true);
|
||||
field.set(instance, fieldValue);
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
// Skip fields that don't exist in the current class version
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a field in a class or its superclasses.
|
||||
*
|
||||
* @param clazz The class to search
|
||||
* @param fieldName The field name to find
|
||||
* @return The found field
|
||||
* @throws NoSuchFieldException If the field is not found
|
||||
*/
|
||||
private Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
|
||||
Class<?> currentClass = clazz;
|
||||
while (currentClass != null) {
|
||||
try {
|
||||
return currentClass.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
currentClass = currentClass.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException("Field not found: " + fieldName);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
/**
|
||||
* Interface for a serializer that uses reflection to handle arbitrary Java objects.
|
||||
*/
|
||||
public interface ReflectionSerializer extends Serializer<Object> {
|
||||
/**
|
||||
* Register a custom serializer for a specific type.
|
||||
*
|
||||
* @param type Type to register
|
||||
* @param serializer Custom serializer for the type
|
||||
* @param <T> Type to register
|
||||
*/
|
||||
<T> void registerSerializer(Class<T> type, TypeSerializer<T> serializer);
|
||||
|
||||
/**
|
||||
* Register a custom deserializer for a specific type.
|
||||
*
|
||||
* @param type Type to register
|
||||
* @param deserializer Custom deserializer for the type
|
||||
* @param <T> Type to register
|
||||
*/
|
||||
<T> void registerDeserializer(Class<T> type, TypeDeserializer<T> deserializer);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
/**
|
||||
* Exception thrown during serialization/deserialization.
|
||||
*/
|
||||
public class SerializationException extends RuntimeException {
|
||||
/**
|
||||
* Create a new serialization exception with a message.
|
||||
*
|
||||
* @param message Error message
|
||||
*/
|
||||
public SerializationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new serialization exception with a message and cause.
|
||||
*
|
||||
* @param message Error message
|
||||
* @param cause Underlying cause
|
||||
*/
|
||||
public SerializationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
/**
|
||||
* Interface for serializing and deserializing objects.
|
||||
*
|
||||
* @param <T> Type of object to serialize/deserialize
|
||||
*/
|
||||
public interface Serializer<T> {
|
||||
/**
|
||||
* Serialize an object to bytes.
|
||||
*
|
||||
* @param obj The object to serialize
|
||||
* @return Serialized bytes
|
||||
*/
|
||||
byte[] serialize(T obj);
|
||||
|
||||
/**
|
||||
* Deserialize bytes to an object.
|
||||
*
|
||||
* @param data The bytes to deserialize
|
||||
* @return Deserialized object
|
||||
*/
|
||||
T deserialize(byte[] data);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
/**
|
||||
* Interface for deserializing a specific type from MessagePack.
|
||||
*
|
||||
* @param <T> Type to deserialize
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TypeDeserializer<T> {
|
||||
/**
|
||||
* Convert from serialized representation to object.
|
||||
*
|
||||
* @param serialized Serialized representation
|
||||
* @return Deserialized object
|
||||
*/
|
||||
T fromSerialized(Object serialized);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
/**
|
||||
* Interface for serializing a specific type to a format that can be included in MessagePack.
|
||||
*
|
||||
* @param <T> Type to serialize
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TypeSerializer<T> {
|
||||
/**
|
||||
* Convert object to a serializable representation.
|
||||
*
|
||||
* @param obj Object to convert
|
||||
* @return Serializable representation (must be compatible with MessagePack)
|
||||
*/
|
||||
Object toSerializable(T obj);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.langgraph.checkpoint.base;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class IDTest {
|
||||
|
||||
@Test
|
||||
public void testUuidDeterministic() {
|
||||
// Same inputs should produce same UUIDs
|
||||
UUID uuid1 = ID.uuid("test", "value");
|
||||
UUID uuid2 = ID.uuid("test", "value");
|
||||
|
||||
assertThat(uuid1).isEqualTo(uuid2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUuidDifferentNamespace() {
|
||||
// Different namespaces should produce different UUIDs
|
||||
UUID uuid1 = ID.uuid("namespace1", "value");
|
||||
UUID uuid2 = ID.uuid("namespace2", "value");
|
||||
|
||||
assertThat(uuid1).isNotEqualTo(uuid2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUuidDifferentName() {
|
||||
// Different names should produce different UUIDs
|
||||
UUID uuid1 = ID.uuid("test", "value1");
|
||||
UUID uuid2 = ID.uuid("test", "value2");
|
||||
|
||||
assertThat(uuid1).isNotEqualTo(uuid2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointId() {
|
||||
// Checkpoint IDs should be valid UUIDs
|
||||
String id = ID.checkpointId("thread-123");
|
||||
|
||||
// Should be a valid UUID string
|
||||
UUID uuid = UUID.fromString(id);
|
||||
assertThat(uuid).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUrlSafeId() {
|
||||
// URL-safe IDs should be deterministic
|
||||
String id1 = ID.urlSafeId("test", "value");
|
||||
String id2 = ID.urlSafeId("test", "value");
|
||||
|
||||
assertThat(id1).isEqualTo(id2);
|
||||
|
||||
// Should not contain padding characters or unsafe URL characters
|
||||
assertThat(id1).doesNotContain("=", "+", "/");
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.langgraph.checkpoint.base.memory;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class AsyncMemoryCheckpointSaverTest {
|
||||
|
||||
private AsyncMemoryCheckpointSaver saver;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
saver = new AsyncMemoryCheckpointSaver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
values.put("key1", "value1");
|
||||
values.put("key2", 42);
|
||||
|
||||
// Create checkpoint asynchronously
|
||||
CompletableFuture<String> future = saver.checkpointAsync(threadId, values);
|
||||
|
||||
// Wait for completion
|
||||
String checkpointId = future.get();
|
||||
|
||||
// Verify checkpoint ID format (should be a UUID)
|
||||
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
|
||||
|
||||
// Verify thread has a checkpoint
|
||||
CompletableFuture<List<String>> listFuture = saver.listAsync(threadId);
|
||||
List<String> checkpoints = listFuture.get();
|
||||
assertThat(checkpoints).hasSize(1);
|
||||
assertThat(checkpoints).contains(checkpointId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetValuesAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
values.put("key1", "value1");
|
||||
values.put("key2", 42);
|
||||
|
||||
// Create checkpoint
|
||||
String checkpointId = saver.checkpointAsync(threadId, values).get();
|
||||
|
||||
// Get values asynchronously
|
||||
CompletableFuture<Optional<Map<String, Object>>> future = saver.getValuesAsync(checkpointId);
|
||||
Optional<Map<String, Object>> retrievedValues = future.get();
|
||||
|
||||
// Verify values
|
||||
assertThat(retrievedValues).isPresent();
|
||||
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
|
||||
assertThat(retrievedValues.get()).containsEntry("key2", 42);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Initially empty
|
||||
CompletableFuture<List<String>> initialFuture = saver.listAsync(threadId);
|
||||
List<String> initial = initialFuture.get();
|
||||
assertThat(initial).isEmpty();
|
||||
|
||||
// Create multiple checkpoints
|
||||
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
|
||||
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
|
||||
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
|
||||
|
||||
// List checkpoints asynchronously
|
||||
CompletableFuture<List<String>> future = saver.listAsync(threadId);
|
||||
List<String> checkpoints = future.get();
|
||||
|
||||
// Verify order and content
|
||||
assertThat(checkpoints).hasSize(3);
|
||||
assertThat(checkpoints).containsExactly(id1, id2, id3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatestAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Initially empty
|
||||
CompletableFuture<Optional<String>> initialFuture = saver.latestAsync(threadId);
|
||||
Optional<String> initial = initialFuture.get();
|
||||
assertThat(initial).isEmpty();
|
||||
|
||||
// Create multiple checkpoints
|
||||
saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
|
||||
saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
|
||||
String id3 = saver.checkpointAsync(threadId, Map.of("key", "value3")).get();
|
||||
|
||||
// Get latest asynchronously
|
||||
CompletableFuture<Optional<String>> future = saver.latestAsync(threadId);
|
||||
Optional<String> latest = future.get();
|
||||
|
||||
// Verify latest
|
||||
assertThat(latest).isPresent();
|
||||
assertThat(latest.get()).isEqualTo(id3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Create checkpoint
|
||||
String checkpointId = saver.checkpointAsync(threadId, Map.of("key", "value")).get();
|
||||
|
||||
// Verify checkpoint exists
|
||||
assertThat(saver.getValuesAsync(checkpointId).get()).isPresent();
|
||||
|
||||
// Delete checkpoint asynchronously
|
||||
CompletableFuture<Void> future = saver.deleteAsync(checkpointId);
|
||||
future.get(); // Wait for completion
|
||||
|
||||
// Verify checkpoint is deleted
|
||||
assertThat(saver.getValuesAsync(checkpointId).get()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearAsync() throws ExecutionException, InterruptedException {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Create multiple checkpoints
|
||||
String id1 = saver.checkpointAsync(threadId, Map.of("key", "value1")).get();
|
||||
String id2 = saver.checkpointAsync(threadId, Map.of("key", "value2")).get();
|
||||
|
||||
// Verify checkpoints exist
|
||||
assertThat(saver.listAsync(threadId).get()).hasSize(2);
|
||||
|
||||
// Clear thread asynchronously
|
||||
CompletableFuture<Void> future = saver.clearAsync(threadId);
|
||||
future.get(); // Wait for completion
|
||||
|
||||
// Verify checkpoints are deleted
|
||||
assertThat(saver.listAsync(threadId).get()).isEmpty();
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package com.langgraph.checkpoint.base.memory;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MemoryCheckpointSaverTest {
|
||||
|
||||
private MemoryCheckpointSaver saver;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
saver = new MemoryCheckpointSaver();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpoint() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
values.put("key1", "value1");
|
||||
values.put("key2", 42);
|
||||
|
||||
// Create checkpoint
|
||||
String checkpointId = saver.checkpoint(threadId, values);
|
||||
|
||||
// Verify checkpoint ID format (should be a UUID)
|
||||
assertThat(checkpointId).matches("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
|
||||
|
||||
// Verify thread has a checkpoint
|
||||
List<String> checkpoints = saver.list(threadId);
|
||||
assertThat(checkpoints).hasSize(1);
|
||||
assertThat(checkpoints).contains(checkpointId);
|
||||
|
||||
// Verify latest checkpoint
|
||||
Optional<String> latest = saver.latest(threadId);
|
||||
assertThat(latest).isPresent();
|
||||
assertThat(latest.get()).isEqualTo(checkpointId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetValues() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
Map<String, Object> values = new HashMap<>();
|
||||
values.put("key1", "value1");
|
||||
values.put("key2", 42);
|
||||
|
||||
// Create checkpoint
|
||||
String checkpointId = saver.checkpoint(threadId, values);
|
||||
|
||||
// Get values
|
||||
Optional<Map<String, Object>> retrievedValues = saver.getValues(checkpointId);
|
||||
|
||||
// Verify values
|
||||
assertThat(retrievedValues).isPresent();
|
||||
assertThat(retrievedValues.get()).containsEntry("key1", "value1");
|
||||
assertThat(retrievedValues.get()).containsEntry("key2", 42);
|
||||
|
||||
// Verify non-existent checkpoint
|
||||
Optional<Map<String, Object>> nonExistent = saver.getValues("non-existent");
|
||||
assertThat(nonExistent).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testList() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Initially empty
|
||||
List<String> initial = saver.list(threadId);
|
||||
assertThat(initial).isEmpty();
|
||||
|
||||
// Create multiple checkpoints
|
||||
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
|
||||
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
|
||||
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
|
||||
|
||||
// List checkpoints
|
||||
List<String> checkpoints = saver.list(threadId);
|
||||
|
||||
// Verify order and content
|
||||
assertThat(checkpoints).hasSize(3);
|
||||
assertThat(checkpoints).containsExactly(id1, id2, id3);
|
||||
|
||||
// Different thread should have no checkpoints
|
||||
List<String> otherThread = saver.list("other-thread");
|
||||
assertThat(otherThread).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLatest() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Initially empty
|
||||
Optional<String> initial = saver.latest(threadId);
|
||||
assertThat(initial).isEmpty();
|
||||
|
||||
// Create multiple checkpoints
|
||||
saver.checkpoint(threadId, Map.of("key", "value1"));
|
||||
saver.checkpoint(threadId, Map.of("key", "value2"));
|
||||
String id3 = saver.checkpoint(threadId, Map.of("key", "value3"));
|
||||
|
||||
// Get latest
|
||||
Optional<String> latest = saver.latest(threadId);
|
||||
|
||||
// Verify latest
|
||||
assertThat(latest).isPresent();
|
||||
assertThat(latest.get()).isEqualTo(id3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDelete() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Create checkpoint
|
||||
String checkpointId = saver.checkpoint(threadId, Map.of("key", "value"));
|
||||
|
||||
// Verify checkpoint exists
|
||||
assertThat(saver.getValues(checkpointId)).isPresent();
|
||||
assertThat(saver.list(threadId)).contains(checkpointId);
|
||||
|
||||
// Delete checkpoint
|
||||
saver.delete(checkpointId);
|
||||
|
||||
// Verify checkpoint is deleted
|
||||
assertThat(saver.getValues(checkpointId)).isEmpty();
|
||||
assertThat(saver.list(threadId)).doesNotContain(checkpointId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClear() {
|
||||
// Create test data
|
||||
String threadId = "test-thread";
|
||||
|
||||
// Create multiple checkpoints
|
||||
String id1 = saver.checkpoint(threadId, Map.of("key", "value1"));
|
||||
String id2 = saver.checkpoint(threadId, Map.of("key", "value2"));
|
||||
|
||||
// Verify checkpoints exist
|
||||
assertThat(saver.list(threadId)).hasSize(2);
|
||||
assertThat(saver.getValues(id1)).isPresent();
|
||||
assertThat(saver.getValues(id2)).isPresent();
|
||||
|
||||
// Clear thread
|
||||
saver.clear(threadId);
|
||||
|
||||
// Verify checkpoints are deleted
|
||||
assertThat(saver.list(threadId)).isEmpty();
|
||||
assertThat(saver.getValues(id1)).isEmpty();
|
||||
assertThat(saver.getValues(id2)).isEmpty();
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
package com.langgraph.checkpoint.serde;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
|
||||
public class MsgPackSerializerTest {
|
||||
|
||||
private MsgPackSerializer serializer;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
serializer = new MsgPackSerializer();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializePrimitives() {
|
||||
// Test with various primitive types
|
||||
assertRoundTrip("Test string");
|
||||
assertRoundTrip(123);
|
||||
assertRoundTrip(123456789L);
|
||||
assertRoundTrip(123.45);
|
||||
assertRoundTrip(123.45f);
|
||||
assertRoundTrip(true);
|
||||
assertRoundTrip(false);
|
||||
assertRoundTrip(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeArrays() {
|
||||
// Test with arrays and collections
|
||||
assertRoundTrip(new byte[] {1, 2, 3, 4, 5});
|
||||
assertRoundTrip(Arrays.asList("one", "two", "three"));
|
||||
assertRoundTrip(Arrays.asList(1, 2, 3, 4, 5));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeMaps() {
|
||||
// Test with maps
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("string", "value");
|
||||
map.put("int", 123);
|
||||
map.put("boolean", true);
|
||||
|
||||
assertRoundTrip(map);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeNestedStructures() {
|
||||
// Test with nested structures
|
||||
Map<String, Object> nested = new HashMap<>();
|
||||
nested.put("list", Arrays.asList(1, 2, 3));
|
||||
nested.put("map", Map.of("key", "value"));
|
||||
|
||||
assertRoundTrip(nested);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeEnums() {
|
||||
// Test with enums
|
||||
assertRoundTrip(TestEnum.VALUE1);
|
||||
assertRoundTrip(TestEnum.VALUE2);
|
||||
assertRoundTrip(TestEnum.VALUE3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeRecord() {
|
||||
// Test with a record
|
||||
TestRecord record = new TestRecord("test", 123, Arrays.asList("a", "b", "c"));
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(record);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(TestRecord.class);
|
||||
TestRecord deserializedRecord = (TestRecord) deserialized;
|
||||
assertThat(deserializedRecord.name()).isEqualTo("test");
|
||||
assertThat(deserializedRecord.value()).isEqualTo(123);
|
||||
assertThat(deserializedRecord.tags()).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeNestedRecord() {
|
||||
// Test with a nested record
|
||||
NestedTestRecord record = new NestedTestRecord(
|
||||
"parent",
|
||||
new TestRecord("child", 456, Arrays.asList("x", "y", "z"))
|
||||
);
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(record);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(NestedTestRecord.class);
|
||||
NestedTestRecord deserializedRecord = (NestedTestRecord) deserialized;
|
||||
assertThat(deserializedRecord.name()).isEqualTo("parent");
|
||||
assertThat(deserializedRecord.child()).isInstanceOf(TestRecord.class);
|
||||
assertThat(deserializedRecord.child().name()).isEqualTo("child");
|
||||
assertThat(deserializedRecord.child().value()).isEqualTo(456);
|
||||
assertThat(deserializedRecord.child().tags()).containsExactly("x", "y", "z");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeCustomObject() {
|
||||
// Test with a custom object
|
||||
TestObject obj = new TestObject();
|
||||
obj.setName("test");
|
||||
obj.setValue(123);
|
||||
obj.setActive(true);
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(obj);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(TestObject.class);
|
||||
TestObject deserializedObj = (TestObject) deserialized;
|
||||
assertThat(deserializedObj.getName()).isEqualTo("test");
|
||||
assertThat(deserializedObj.getValue()).isEqualTo(123);
|
||||
assertThat(deserializedObj.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeInheritance() {
|
||||
// Test with inheritance
|
||||
ChildTestObject obj = new ChildTestObject();
|
||||
obj.setName("parent");
|
||||
obj.setValue(123);
|
||||
obj.setActive(true);
|
||||
obj.setChildProperty("child");
|
||||
obj.setChildValue(456);
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(obj);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(ChildTestObject.class);
|
||||
ChildTestObject deserializedObj = (ChildTestObject) deserialized;
|
||||
assertThat(deserializedObj.getName()).isEqualTo("parent");
|
||||
assertThat(deserializedObj.getValue()).isEqualTo(123);
|
||||
assertThat(deserializedObj.isActive()).isTrue();
|
||||
assertThat(deserializedObj.getChildProperty()).isEqualTo("child");
|
||||
assertThat(deserializedObj.getChildValue()).isEqualTo(456);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeWithCustomSerializer() {
|
||||
// Register custom UUID serializer (although built-in one exists)
|
||||
serializer.registerSerializer(UUID.class, (uuid) -> uuid.toString().replace("-", ""));
|
||||
serializer.registerDeserializer(UUID.class, (str) -> {
|
||||
String uuidStr = (String) str;
|
||||
// Insert hyphens for standard UUID format
|
||||
uuidStr = uuidStr.replaceFirst(
|
||||
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
|
||||
"$1-$2-$3-$4-$5");
|
||||
return UUID.fromString(uuidStr);
|
||||
});
|
||||
|
||||
// Test with UUID
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(uuid);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(UUID.class);
|
||||
assertThat(deserialized).isEqualTo(uuid);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeDeserializeDateTypes() {
|
||||
// Test with Date
|
||||
Date date = new Date();
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(date);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(Date.class);
|
||||
assertThat(deserialized).isEqualTo(date);
|
||||
|
||||
// Test with Java 8 Date/Time types
|
||||
Instant instant = Instant.now();
|
||||
LocalDate localDate = LocalDate.now();
|
||||
LocalTime localTime = LocalTime.now();
|
||||
LocalDateTime localDateTime = LocalDateTime.now();
|
||||
|
||||
assertRoundTrip(instant);
|
||||
assertRoundTrip(localDate);
|
||||
assertRoundTrip(localTime);
|
||||
assertRoundTrip(localDateTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTransientFields() {
|
||||
// Test with transient fields
|
||||
ObjectWithTransient obj = new ObjectWithTransient();
|
||||
obj.setPersistent("saved");
|
||||
obj.setTransientField("not-saved");
|
||||
|
||||
// Serialize and deserialize
|
||||
byte[] serialized = serializer.serialize(obj);
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
assertThat(deserialized).isInstanceOf(ObjectWithTransient.class);
|
||||
ObjectWithTransient deserializedObj = (ObjectWithTransient) deserialized;
|
||||
assertThat(deserializedObj.getPersistent()).isEqualTo("saved");
|
||||
assertThat(deserializedObj.getTransientField()).isNull(); // Should be null after deserialization
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to assert that an object survives a round trip through serialization.
|
||||
*
|
||||
* @param obj Object to test
|
||||
*/
|
||||
private void assertRoundTrip(Object obj) {
|
||||
try {
|
||||
// Serialize
|
||||
byte[] serialized = serializer.serialize(obj);
|
||||
|
||||
// Deserialize
|
||||
Object deserialized = serializer.deserialize(serialized);
|
||||
|
||||
// Verify
|
||||
if (obj instanceof byte[]) {
|
||||
// Arrays need special comparison
|
||||
assertThat(deserialized).isInstanceOf(byte[].class);
|
||||
assertThat((byte[]) deserialized).isEqualTo((byte[]) obj);
|
||||
} else if (obj instanceof Number) {
|
||||
// For any number type, compare by value instead of exact type
|
||||
if (deserialized instanceof Number) {
|
||||
double expected = ((Number) obj).doubleValue();
|
||||
double actual = ((Number) deserialized).doubleValue();
|
||||
assertThat(actual).isCloseTo(expected, within(0.0001));
|
||||
} else {
|
||||
throw new AssertionError("Expected Number, got " +
|
||||
(deserialized != null ? deserialized.getClass().getName() : "null"));
|
||||
}
|
||||
} else {
|
||||
// Special handling for lists
|
||||
if (obj instanceof List && deserialized instanceof List) {
|
||||
List<?> originalList = (List<?>) obj;
|
||||
List<?> deserializedList = (List<?>) deserialized;
|
||||
assertThat(deserializedList).hasSameSizeAs(originalList);
|
||||
|
||||
// Check each element
|
||||
for (int i = 0; i < originalList.size(); i++) {
|
||||
Object originalItem = originalList.get(i);
|
||||
Object deserializedItem = deserializedList.get(i);
|
||||
|
||||
if (originalItem instanceof Number && deserializedItem instanceof Number) {
|
||||
// Compare numbers by value instead of exact type
|
||||
assertThat(((Number) deserializedItem).doubleValue())
|
||||
.isCloseTo(((Number) originalItem).doubleValue(), within(0.0001));
|
||||
} else {
|
||||
assertThat(deserializedItem).isEqualTo(originalItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Regular equality for other types
|
||||
assertThat(deserialized).isEqualTo(obj);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError("Error in roundtrip for " + obj + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test enum.
|
||||
*/
|
||||
public enum TestEnum {
|
||||
VALUE1, VALUE2, VALUE3
|
||||
}
|
||||
|
||||
/**
|
||||
* Test record class.
|
||||
*/
|
||||
public record TestRecord(String name, int value, List<String> tags) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested test record class.
|
||||
*/
|
||||
public record NestedTestRecord(String name, TestRecord child) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Test class for custom object serialization.
|
||||
*/
|
||||
public static class TestObject {
|
||||
private String name;
|
||||
private int value;
|
||||
private boolean active;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Child test class for inheritance testing.
|
||||
*/
|
||||
public static class ChildTestObject extends TestObject {
|
||||
private String childProperty;
|
||||
private int childValue;
|
||||
|
||||
public String getChildProperty() {
|
||||
return childProperty;
|
||||
}
|
||||
|
||||
public void setChildProperty(String childProperty) {
|
||||
this.childProperty = childProperty;
|
||||
}
|
||||
|
||||
public int getChildValue() {
|
||||
return childValue;
|
||||
}
|
||||
|
||||
public void setChildValue(int childValue) {
|
||||
this.childValue = childValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test class with transient fields.
|
||||
*/
|
||||
public static class ObjectWithTransient {
|
||||
private String persistent;
|
||||
private transient String transientField;
|
||||
|
||||
public String getPersistent() {
|
||||
return persistent;
|
||||
}
|
||||
|
||||
public void setPersistent(String persistent) {
|
||||
this.persistent = persistent;
|
||||
}
|
||||
|
||||
public String getTransientField() {
|
||||
return transientField;
|
||||
}
|
||||
|
||||
public void setTransientField(String transientField) {
|
||||
this.transientField = transientField;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
rootProject.name = 'langgraph-java'
|
||||
|
||||
include 'langgraph-checkpoint'
|
||||
include 'langgraph-core'
|
||||
include 'langgraph-examples'
|
||||
Reference in New Issue
Block a user