From 71db4a8a5b0a2b5c38e0ced3061623596d9da251 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Thu, 17 Oct 2024 11:54:05 -0700 Subject: [PATCH 1/5] chore(checkpoint): Release 0.0.11 (#611) --- libs/checkpoint/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/package.json b/libs/checkpoint/package.json index c080f898..b5022a1c 100644 --- a/libs/checkpoint/package.json +++ b/libs/checkpoint/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-checkpoint", - "version": "0.0.10", + "version": "0.0.11", "description": "Library with base interfaces for LangGraph checkpoint savers.", "type": "module", "engines": { From 2312f1ae3ac1bbe366f15129ace53d5d887f537e Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Thu, 17 Oct 2024 11:58:06 -0700 Subject: [PATCH 2/5] chore(checkpoint-mongodb): Release 0.0.4 (#612) --- libs/checkpoint-mongodb/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-mongodb/package.json b/libs/checkpoint-mongodb/package.json index 41c636d7..4c16f852 100644 --- a/libs/checkpoint-mongodb/package.json +++ b/libs/checkpoint-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-checkpoint-mongodb", - "version": "0.0.3", + "version": "0.0.4", "description": "LangGraph", "type": "module", "engines": { From c98c65a880b0ae9911f64fbac9890da45fb309f2 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Thu, 17 Oct 2024 12:37:52 -0700 Subject: [PATCH 3/5] chore(checkpoint-postgres): Release 0.0.2 (#613) --- libs/checkpoint-postgres/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/package.json b/libs/checkpoint-postgres/package.json index c98d94d6..2a7304a6 100644 --- a/libs/checkpoint-postgres/package.json +++ b/libs/checkpoint-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-checkpoint-postgres", - "version": "0.0.1", + "version": "0.0.2", "description": "LangGraph", "type": "module", "engines": { From d17226c10af3dd8d18b797280c369b591a7fad5d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 18 Oct 2024 14:54:02 -0700 Subject: [PATCH 4/5] Fix docstring for Checkpoint.v (#615) --- libs/checkpoint/src/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/checkpoint/src/base.ts b/libs/checkpoint/src/base.ts index a0057990..5382b4e5 100644 --- a/libs/checkpoint/src/base.ts +++ b/libs/checkpoint/src/base.ts @@ -23,7 +23,7 @@ export interface Checkpoint< C extends string = string > { /** - * Version number + * The version of the checkpoint format. Currently 1 */ v: number; /** From b76700e9dc47dcc47338e023559736dbde9182f8 Mon Sep 17 00:00:00 2001 From: Ben Burns <803016+benjamincburns@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:23:46 +1300 Subject: [PATCH 5/5] fix(checkpoint-sqlite): list method bug fixes (#582) --- libs/checkpoint-sqlite/src/index.ts | 92 +++++++++++++++++-- .../src/tests/checkpoints.test.ts | 26 ++++-- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/libs/checkpoint-sqlite/src/index.ts b/libs/checkpoint-sqlite/src/index.ts index bda2b88e..6740bb22 100644 --- a/libs/checkpoint-sqlite/src/index.ts +++ b/libs/checkpoint-sqlite/src/index.ts @@ -31,6 +31,33 @@ interface WritesRow { value?: string; } +// In the `SqliteSaver.list` method, we need to sanitize the `options.filter` argument to ensure it only contains keys +// that are part of the `CheckpointMetadata` type. The lines below ensure that we get compile-time errors if the list +// of keys that we use is out of sync with the `CheckpointMetadata` type. +const checkpointMetadataKeys = ["source", "step", "writes", "parents"] as const; + +type CheckKeys = [K[number]] extends [ + keyof T +] + ? [keyof T] extends [K[number]] + ? K + : never + : never; + +function validateKeys( + keys: CheckKeys +): K { + return keys; +} + +// If this line fails to compile, the list of keys that we use in the `SqliteSaver.list` method is out of sync with the +// `CheckpointMetadata` type. In that case, just update `checkpointMetadataKeys` to contain all the keys in +// `CheckpointMetadata` +const validCheckpointMetadataKeys = validateKeys< + CheckpointMetadata, + typeof checkpointMetadataKeys +>(checkpointMetadataKeys); + export class SqliteSaver extends BaseCheckpointSaver { db: DatabaseType; @@ -165,19 +192,68 @@ CREATE TABLE IF NOT EXISTS writes ( config: RunnableConfig, options?: CheckpointListOptions ): AsyncGenerator { - const { limit, before } = options ?? {}; + const { limit, before, filter } = options ?? {}; this.setup(); const thread_id = config.configurable?.thread_id; - let sql = `SELECT thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata FROM checkpoints WHERE thread_id = ? ${ - before ? "AND checkpoint_id < ?" : "" - } ORDER BY checkpoint_id DESC`; - if (limit) { - sql += ` LIMIT ${limit}`; + const checkpoint_ns = config.configurable?.checkpoint_ns; + + let sql = + `SELECT\n` + + " thread_id,\n" + + " checkpoint_ns,\n" + + " checkpoint_id,\n" + + " parent_checkpoint_id,\n" + + " type,\n" + + " checkpoint,\n" + + " metadata\n" + + "FROM checkpoints\n"; + + const whereClause: string[] = []; + + if (thread_id) { + whereClause.push("thread_id = ?"); + } + + if (checkpoint_ns !== undefined && checkpoint_ns !== null) { + whereClause.push("checkpoint_ns = ?"); + } + + if (before?.configurable?.checkpoint_id !== undefined) { + whereClause.push("checkpoint_id < ?"); } - const args = [thread_id, before?.configurable?.checkpoint_id].filter( - Boolean + + const sanitizedFilter = Object.fromEntries( + Object.entries(filter ?? {}).filter( + ([key, value]) => + value !== undefined && + validCheckpointMetadataKeys.includes(key as keyof CheckpointMetadata) + ) ); + whereClause.push( + ...Object.entries(sanitizedFilter).map( + ([key]) => `jsonb(CAST(metadata AS TEXT))->'$.${key}' = ?` + ) + ); + + if (whereClause.length > 0) { + sql += `WHERE\n ${whereClause.join(" AND\n ")}\n`; + } + + sql += "\nORDER BY checkpoint_id DESC"; + + if (limit) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sql += ` LIMIT ${parseInt(limit as any, 10)}`; // parseInt here (with cast to make TS happy) to sanitize input, as limit may be user-provided + } + + const args = [ + thread_id, + checkpoint_ns, + before?.configurable?.checkpoint_id, + ...Object.values(sanitizedFilter).map((value) => JSON.stringify(value)), + ].filter((value) => value !== undefined && value !== null); + const rows: CheckpointRow[] = this.db .prepare(sql) .all(...args) as CheckpointRow[]; diff --git a/libs/checkpoint-sqlite/src/tests/checkpoints.test.ts b/libs/checkpoint-sqlite/src/tests/checkpoints.test.ts index ca5c6d72..3cc08830 100644 --- a/libs/checkpoint-sqlite/src/tests/checkpoints.test.ts +++ b/libs/checkpoint-sqlite/src/tests/checkpoints.test.ts @@ -103,7 +103,12 @@ describe("SqliteSaver", () => { }, }, checkpoint2, - { source: "update", step: -1, writes: null, parents: {} } + { + source: "update", + step: -1, + writes: null, + parents: { "": checkpoint1.id }, + } ); // verify that parentTs is set and retrieved correctly for second checkpoint @@ -119,18 +124,25 @@ describe("SqliteSaver", () => { }); // list checkpoints - const checkpointTupleGenerator = await sqliteSaver.list({ - configurable: { thread_id: "1" }, - }); + const checkpointTupleGenerator = await sqliteSaver.list( + { + configurable: { thread_id: "1" }, + }, + { + filter: { + source: "update", + step: -1, + parents: { "": checkpoint1.id }, + }, + } + ); const checkpointTuples: CheckpointTuple[] = []; for await (const checkpoint of checkpointTupleGenerator) { checkpointTuples.push(checkpoint); } - expect(checkpointTuples.length).toBe(2); + expect(checkpointTuples.length).toBe(1); const checkpointTuple1 = checkpointTuples[0]; - const checkpointTuple2 = checkpointTuples[1]; expect(checkpointTuple1.checkpoint.ts).toBe("2024-04-20T17:19:07.952Z"); - expect(checkpointTuple2.checkpoint.ts).toBe("2024-04-19T17:19:07.952Z"); }); });