Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion langgraph/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"license": "MIT",
"dependencies": {
"@langchain/core": "^0.1.61",
"better-sqlite3": "^9.5.0"
"better-sqlite3": "^9.5.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@jest/globals": "^29.5.0",
Expand All @@ -49,6 +50,7 @@
"@swc/jest": "^0.2.29",
"@tsconfig/recommended": "^1.0.3",
"@types/better-sqlite3": "^7.6.9",
"@types/uuid": "^9",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"dotenv": "^16.3.1",
Expand Down
5 changes: 4 additions & 1 deletion langgraph/src/channels/base.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { deepCopy } from "../checkpoint/base.js";
import { uuid6 } from "../checkpoint/id.js";
import { Checkpoint } from "../checkpoint/index.js";
import { EmptyChannelError } from "../errors.js";

Expand Down Expand Up @@ -65,7 +66,8 @@ export function emptyChannels<Cc extends Record<string, BaseChannel>>(

export function createCheckpoint<ValueType>(
checkpoint: Checkpoint,
channels: Record<string, BaseChannel<ValueType>>
channels: Record<string, BaseChannel<ValueType>>,
step: number
): Checkpoint {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const values: Record<string, any> = {};
Expand All @@ -83,6 +85,7 @@ export function createCheckpoint<ValueType>(
}
return {
v: 1,
id: uuid6(step),
ts: new Date().toISOString(),
channel_values: values,
channel_versions: { ...checkpoint.channel_versions },
Expand Down
7 changes: 7 additions & 0 deletions langgraph/src/checkpoint/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RunnableConfig } from "@langchain/core/runnables";
import { SerializerProtocol } from "../serde/base.js";
import { uuid6 } from "./id.js";

export interface CheckpointMetadata {
source: "input" | "loop" | "update";
Expand All @@ -26,6 +27,10 @@ export interface Checkpoint {
* Version number
*/
v: number;
/**
* Checkpoint ID {uuid6}
*/
id: string;
/**
* Timestamp {new Date().toISOString()}
*/
Expand Down Expand Up @@ -66,6 +71,7 @@ export function deepCopy<T>(obj: T): T {
export function emptyCheckpoint(): Checkpoint {
return {
v: 1,
id: uuid6(-2),
ts: new Date().toISOString(),
channel_values: {},
channel_versions: {},
Expand All @@ -76,6 +82,7 @@ export function emptyCheckpoint(): Checkpoint {
export function copyCheckpoint(checkpoint: Checkpoint): Checkpoint {
return {
v: checkpoint.v,
id: checkpoint.id,
ts: checkpoint.ts,
channel_values: { ...checkpoint.channel_values },
channel_versions: { ...checkpoint.channel_versions },
Expand Down
44 changes: 44 additions & 0 deletions langgraph/src/checkpoint/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { v1 } from "uuid";

/**
* Returns an unsigned `x`-bit random integer.
* @param x - An unsigned integer ranging from 0 to 53, inclusive.
* @returns An unsigned `x`-bit random integer (`0 <= f(x) < 2^x`).
*/
function getRandomInt(x: number): number {
if (x < 0 || x > 53) {
return NaN;
}
const n = 0 | (Math.random() * 0x40000000); // 1 << 30
return x > 30
? n + (0 | (Math.random() * (1 << (x - 30)))) * 0x40000000
: n >>> (30 - x);
}

export function uuid6(clockseq: number): string {
const node =
typeof crypto !== "undefined"
? crypto.getRandomValues(new Uint8Array(6))
: [
getRandomInt(8),
getRandomInt(8),
getRandomInt(8),
getRandomInt(8),
getRandomInt(8),
getRandomInt(8),
];
const uuid1 = v1({ node, clockseq });
return convert1to6(uuid1);
}

export function convert1to6(uuid1: string): string {
// https://github.com/oittaa/uuid6-python/blob/main/src/uuid6/__init__.py#L81
const hex = uuid1.replace(/-/g, "");
const v6 = `${hex.slice(13, 16)}${hex.slice(8, 12)}${hex.slice(
0,
1
)}-${hex.slice(1, 5)}-6${hex.slice(5, 8)}-${hex.slice(16, 20)}-${hex.slice(
20
)}`;
return v6;
}
6 changes: 3 additions & 3 deletions langgraph/src/checkpoint/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export class MemorySaver extends BaseCheckpointSaver {
const thread_id = config.configurable?.thread_id;

if (this.storage[thread_id]) {
this.storage[thread_id][checkpoint.ts] = [
this.storage[thread_id][checkpoint.id] = [
this.serde.stringify(checkpoint),
this.serde.stringify(metadata),
];
} else {
this.storage[thread_id] = {
[checkpoint.ts]: [
[checkpoint.id]: [
this.serde.stringify(checkpoint),
this.serde.stringify(metadata),
],
Expand All @@ -86,7 +86,7 @@ export class MemorySaver extends BaseCheckpointSaver {
return {
configurable: {
thread_id,
checkpoint_id: checkpoint.ts,
checkpoint_id: checkpoint.id,
},
};
}
Expand Down
4 changes: 2 additions & 2 deletions langgraph/src/checkpoint/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ CREATE TABLE IF NOT EXISTS checkpoints (
try {
const row = [
config.configurable?.thread_id,
checkpoint.ts,
checkpoint.id,
config.configurable?.checkpoint_id,
this.serde.stringify(checkpoint),
this.serde.stringify(metadata),
Expand All @@ -188,7 +188,7 @@ CREATE TABLE IF NOT EXISTS checkpoints (
return {
configurable: {
thread_id: config.configurable?.thread_id,
checkpoint_id: checkpoint.ts,
checkpoint_id: checkpoint.id,
},
};
}
Expand Down
10 changes: 5 additions & 5 deletions langgraph/src/pregel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export class Pregel<
_applyWrites(checkpoint, channels, inputPendingWrites);
// save input checkpoint
if (this.checkpointer) {
checkpoint = createCheckpoint(checkpoint, channels);
checkpoint = createCheckpoint(checkpoint, channels, start);
bg.push(
this.checkpointer.put(checkpointConfig, checkpoint, {
source: "input",
Expand All @@ -451,7 +451,7 @@ export class Pregel<
checkpointConfig = {
configurable: {
...checkpointConfig.configurable,
checkpoint_id: checkpoint.ts,
checkpoint_id: checkpoint.id,
},
};
}
Expand Down Expand Up @@ -553,7 +553,7 @@ export class Pregel<

// save end of step checkpoint
if (this.checkpointer) {
checkpoint = createCheckpoint(checkpoint, channels);
checkpoint = createCheckpoint(checkpoint, channels, step);
bg.push(
this.checkpointer.put(checkpointConfig, checkpoint, {
source: "loop",
Expand All @@ -568,7 +568,7 @@ export class Pregel<
checkpointConfig = {
configurable: {
...checkpointConfig.configurable,
checkpoint_id: checkpoint.ts,
checkpoint_id: checkpoint.id,
},
};
}
Expand Down Expand Up @@ -684,7 +684,7 @@ export function _localRead(
fresh: boolean = false
): Record<string, unknown> | unknown {
if (fresh) {
const newCheckpoint = createCheckpoint(checkpoint, channels);
const newCheckpoint = createCheckpoint(checkpoint, channels, -1);
// create a new copy of channels
const newChannels = emptyChannels(channels, newCheckpoint);
// Note: _applyWrites contains side effects
Expand Down
35 changes: 31 additions & 4 deletions langgraph/src/tests/checkpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { describe, it, expect } from "@jest/globals";
import { Checkpoint, CheckpointTuple, deepCopy } from "../checkpoint/base.js";
import { MemorySaver } from "../checkpoint/memory.js";
import { SqliteSaver } from "../checkpoint/sqlite.js";
import { convert1to6, uuid6 } from "../checkpoint/id.js";

const checkpoint1: Checkpoint = {
v: 1,
id: uuid6(-1),
ts: "2024-04-19T17:19:07.952Z",
channel_values: {
someKey1: "someValue1",
Expand All @@ -20,6 +22,7 @@ const checkpoint1: Checkpoint = {
};
const checkpoint2: Checkpoint = {
v: 1,
id: uuid6(1),
ts: "2024-04-20T17:19:07.952Z",
channel_values: {
someKey1: "someValue2",
Expand Down Expand Up @@ -87,7 +90,7 @@ describe("MemorySaver", () => {
expect(runnableConfig).toEqual({
configurable: {
thread_id: "1",
checkpoint_id: "2024-04-19T17:19:07.952Z",
checkpoint_id: checkpoint1.id,
},
});

Expand All @@ -98,7 +101,7 @@ describe("MemorySaver", () => {
expect(checkpointTuple?.config).toEqual({
configurable: {
thread_id: "1",
checkpoint_id: "2024-04-19T17:19:07.952Z",
checkpoint_id: checkpoint1.id,
},
});
expect(checkpointTuple?.checkpoint).toEqual(checkpoint1);
Expand Down Expand Up @@ -145,7 +148,7 @@ describe("SqliteSaver", () => {
expect(runnableConfig).toEqual({
configurable: {
thread_id: "1",
checkpoint_id: "2024-04-19T17:19:07.952Z",
checkpoint_id: checkpoint1.id,
},
});

Expand All @@ -156,7 +159,7 @@ describe("SqliteSaver", () => {
expect(firstCheckpointTuple?.config).toEqual({
configurable: {
thread_id: "1",
checkpoint_id: "2024-04-19T17:19:07.952Z",
checkpoint_id: checkpoint1.id,
},
});
expect(firstCheckpointTuple?.checkpoint).toEqual(checkpoint1);
Expand Down Expand Up @@ -201,3 +204,27 @@ describe("SqliteSaver", () => {
expect(checkpointTuple2.checkpoint.ts).toBe("2024-04-19T17:19:07.952Z");
});
});

describe("id", () => {
it("should convert uuid1 to uuid6", () => {
const regex =
/^[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;

// [UUIDv1, UUIDv6]
const cases = [
[
"5714f720-1268-11e7-a24b-96d95aa38c32",
"1e712685-714f-6720-a24b-96d95aa38c32",
],
[
"68f820c0-1268-11e7-a24b-671acd892c6a",
"1e712686-8f82-60c0-a24b-671acd892c6a",
],
];
cases.forEach(([v1, v6]) => {
const converted = convert1to6(v1);
expect(converted).toBe(v6);
expect(converted).toMatch(regex);
});
});
});
Loading