diff --git a/libs/langgraph-api/.eslintrc.cjs b/libs/langgraph-api/.eslintrc.cjs new file mode 100644 index 000000000..3b111c39f --- /dev/null +++ b/libs/langgraph-api/.eslintrc.cjs @@ -0,0 +1,68 @@ +module.exports = { + extends: [ + "airbnb-base", + "eslint:recommended", + "prettier", + "plugin:@typescript-eslint/recommended", + ], + parserOptions: { + ecmaVersion: 12, + parser: "@typescript-eslint/parser", + project: "./tsconfig.json", + sourceType: "module", + }, + plugins: ["@typescript-eslint", "no-instanceof"], + ignorePatterns: [ + ".eslintrc.cjs", + "scripts", + "node_modules", + "dist", + "dist-cjs", + "*.js", + "*.cjs", + "*.d.ts", + ], + rules: { + "no-process-env": 2, + "no-instanceof/no-instanceof": 2, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-shadow": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-use-before-define": ["error", "nofunc"], + "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }], + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "arrow-body-style": 0, + camelcase: 0, + "class-methods-use-this": 0, + "import/extensions": [2, "ignorePackages"], + "import/no-extraneous-dependencies": [ + "error", + { devDependencies: ["**/*.test.ts"] }, + ], + "import/no-unresolved": 0, + "import/prefer-default-export": 0, + "keyword-spacing": "error", + "max-classes-per-file": 0, + "max-len": 0, + "no-await-in-loop": 0, + "no-bitwise": 0, + "no-console": 0, + "no-empty-function": 0, + "no-restricted-syntax": 0, + "no-shadow": 0, + "no-continue": 0, + "no-void": 0, + "no-underscore-dangle": 0, + "no-use-before-define": 0, + "no-useless-constructor": 0, + "no-return-await": 0, + "consistent-return": 0, + "no-else-return": 0, + "func-names": 0, + "no-lonely-if": 0, + "prefer-rest-params": 0, + "new-cap": ["error", { properties: false, capIsNew: false }], + }, +}; diff --git a/libs/langgraph-api/package.json b/libs/langgraph-api/package.json index 45ef7177c..4ce2b71ff 100644 --- a/libs/langgraph-api/package.json +++ b/libs/langgraph-api/package.json @@ -42,6 +42,10 @@ "build": "yarn turbo:command build:internal --filter=@langchain/langgraph-api", "build:internal": "yarn clean && node scripts/build.mjs", "dev": "tsx ./tests/utils.server.mts --dev", + "lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js,.mts,.mjs src/", + "lint:dpdm": "dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts src/*.js src/**/*.js src/*.mts src/**/*.mts src/*.mjs src/**/*.mjs", + "lint": "yarn lint:eslint && yarn lint:dpdm", + "lint:fix": "yarn lint:eslint --fix && yarn lint:dpdm", "prepack": "yarn run build", "typecheck": "tsc --noEmit", "test": "vitest run", @@ -93,6 +97,13 @@ "@types/react-dom": "^19.0.3", "@types/semver": "^7.7.0", "@types/uuid": "^10.0.0", + "dpdm": "^3.12.0", + "eslint": "^8.33.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-no-instanceof": "^1.0.1", + "eslint-plugin-prettier": "^4.2.1", "jose": "^6.0.10", "postgres": "^3.4.5", "prettier": "^2.8.3", diff --git a/libs/langgraph-api/src/api/assistants.mts b/libs/langgraph-api/src/api/assistants.mts index 1d8b362fd..ca7220019 100644 --- a/libs/langgraph-api/src/api/assistants.mts +++ b/libs/langgraph-api/src/api/assistants.mts @@ -4,6 +4,7 @@ import { Hono } from "hono"; import { v4 as uuid } from "uuid"; import { z } from "zod"; +import { HTTPException } from "hono/http-exception"; import { getAssistantId, getCachedStaticGraphSchema, @@ -11,9 +12,9 @@ import { } from "../graph/load.mjs"; import { getRuntimeGraphSchema } from "../graph/parser/index.mjs"; -import { HTTPException } from "hono/http-exception"; import * as schemas from "../schemas.mjs"; import { Assistants } from "../storage/ops.mjs"; + const api = new Hono(); const RunnableConfigSchema = z.object({ diff --git a/libs/langgraph-api/src/api/meta.mts b/libs/langgraph-api/src/api/meta.mts index a8fdcb827..7e3c916b4 100644 --- a/libs/langgraph-api/src/api/meta.mts +++ b/libs/langgraph-api/src/api/meta.mts @@ -31,19 +31,19 @@ try { } // read env variable -const env = process.env; +const {env} = process; api.get("/info", (c) => { - const langsmithApiKey = env["LANGSMITH_API_KEY"] || env["LANGCHAIN_API_KEY"]; + const langsmithApiKey = env.LANGSMITH_API_KEY || env.LANGCHAIN_API_KEY; const langsmithTracing = (() => { if (langsmithApiKey) { // Check if any tracing variable is explicitly set to "false" const tracingVars = [ - env["LANGCHAIN_TRACING_V2"], - env["LANGCHAIN_TRACING"], - env["LANGSMITH_TRACING_V2"], - env["LANGSMITH_TRACING"], + env.LANGCHAIN_TRACING_V2, + env.LANGCHAIN_TRACING, + env.LANGSMITH_TRACING_V2, + env.LANGSMITH_TRACING, ]; // Return true unless explicitly disabled diff --git a/libs/langgraph-api/src/api/runs.mts b/libs/langgraph-api/src/api/runs.mts index 893deaffa..a854c6455 100644 --- a/libs/langgraph-api/src/api/runs.mts +++ b/libs/langgraph-api/src/api/runs.mts @@ -81,9 +81,9 @@ const createValidRun = async ( if (auth) { userId = auth.user.identity ?? auth.user.id; config.configurable ??= {}; - config.configurable["langgraph_auth_user"] = auth.user; - config.configurable["langgraph_auth_user_id"] = userId; - config.configurable["langgraph_auth_permissions"] = auth.scopes; + config.configurable.langgraph_auth_user = auth.user; + config.configurable.langgraph_auth_user_id = userId; + config.configurable.langgraph_auth_permissions = auth.scopes; } let feedbackKeys = diff --git a/libs/langgraph-api/src/api/store.mts b/libs/langgraph-api/src/api/store.mts index 7cdaa284a..7e3963ab2 100644 --- a/libs/langgraph-api/src/api/store.mts +++ b/libs/langgraph-api/src/api/store.mts @@ -1,9 +1,9 @@ import { Hono } from "hono"; import { zValidator } from "@hono/zod-validator"; -import * as schemas from "../schemas.mjs"; import { HTTPException } from "hono/http-exception"; -import { store as storageStore } from "../storage/store.mjs"; import type { Item } from "@langchain/langgraph"; +import * as schemas from "../schemas.mjs"; +import { store as storageStore } from "../storage/store.mjs"; import { handleAuthEvent } from "../auth/custom.mjs"; const api = new Hono(); @@ -17,8 +17,8 @@ const validateNamespace = (namespace: string[]) => { if (!label || label.includes(".")) { throw new HTTPException(422, { message: - "Namespace labels cannot be empty or contain periods. Received: " + - namespace.join("."), + `Namespace labels cannot be empty or contain periods. Received: ${ + namespace.join(".")}`, }); } } @@ -136,8 +136,8 @@ api.get( key: payload.key, }); - const key = payload.key; - const namespace = payload.namespace; + const {key} = payload; + const {namespace} = payload; return c.json(mapItemsToApi(await storageStore.get(namespace, key))); } ); diff --git a/libs/langgraph-api/src/cli/entrypoint.mts b/libs/langgraph-api/src/cli/entrypoint.mts index fefdf9468..8bbd14148 100644 --- a/libs/langgraph-api/src/cli/entrypoint.mts +++ b/libs/langgraph-api/src/cli/entrypoint.mts @@ -1,8 +1,8 @@ import { asyncExitHook } from "exit-hook"; import * as process from "node:process"; +import { Client as LangSmithClient } from "langsmith"; import { startServer, StartServerSchema } from "../server.mjs"; import { connectToServer } from "./utils/ipc/client.mjs"; -import { Client as LangSmithClient } from "langsmith"; import { logger } from "../logging.mjs"; logger.info(`Starting server...`); diff --git a/libs/langgraph-api/src/command.mts b/libs/langgraph-api/src/command.mts index c49a9c747..0a2f87fe5 100644 --- a/libs/langgraph-api/src/command.mts +++ b/libs/langgraph-api/src/command.mts @@ -12,7 +12,7 @@ export interface RunCommand { } export const getLangGraphCommand = (command: RunCommand) => { - let goto = + const goto = command.goto != null && !Array.isArray(command.goto) ? [command.goto] : command.goto; diff --git a/libs/langgraph-api/src/experimental/embed.mts b/libs/langgraph-api/src/experimental/embed.mts index bc59bcdbd..4c72e2f46 100644 --- a/libs/langgraph-api/src/experimental/embed.mts +++ b/libs/langgraph-api/src/experimental/embed.mts @@ -9,10 +9,10 @@ import { streamSSE } from "hono/streaming"; import { RunnableConfig } from "@langchain/core/runnables"; import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; import type { Metadata, Run } from "../storage/ops.mjs"; import * as schemas from "../schemas.mjs"; -import { z } from "zod"; import { streamState } from "../stream.mjs"; import { serialiseAsDict, serializeError } from "../utils/serde.mjs"; import { getDisconnectAbortSignal, jsonExtra } from "../utils/hono.mjs"; @@ -62,11 +62,10 @@ function createStubRun( : undefined; if (streamMode == null || streamMode.length === 0) streamMode = ["values"]; - const config = Object.assign( - {}, - payload.config ?? {}, - { - configurable: { + const config = { + + ...payload.config ?? {}, + configurable: { run_id: runId, thread_id: threadId, graph_id: payload.assistant_id, @@ -81,9 +80,8 @@ function createStubRun( } : null), }, - }, - { metadata: payload.metadata ?? {} } - ); + metadata: payload.metadata ?? {} + }; return { run_id: runId, diff --git a/libs/langgraph-api/src/graph/load.mts b/libs/langgraph-api/src/graph/load.mts index a64fab152..b9f99f6e4 100644 --- a/libs/langgraph-api/src/graph/load.mts +++ b/libs/langgraph-api/src/graph/load.mts @@ -1,7 +1,6 @@ import { z } from "zod"; import * as uuid from "uuid"; -import { Assistants } from "../storage/ops.mjs"; import type { BaseCheckpointSaver, BaseStore, @@ -9,6 +8,7 @@ import type { LangGraphRunnableConfig, } from "@langchain/langgraph"; import { HTTPException } from "hono/http-exception"; +import { Assistants } from "../storage/ops.mjs"; import { type CompiledGraphFactory, resolveGraph } from "./load.utils.mjs"; import type { GraphSchema, GraphSpec } from "./parser/index.mjs"; import { getStaticGraphSchema } from "./parser/index.mjs"; diff --git a/libs/langgraph-api/src/graph/parser/parser.mts b/libs/langgraph-api/src/graph/parser/parser.mts index 24bf5a2c4..20bb690e8 100644 --- a/libs/langgraph-api/src/graph/parser/parser.mts +++ b/libs/langgraph-api/src/graph/parser/parser.mts @@ -2,9 +2,9 @@ import * as ts from "typescript"; import * as vfs from "@typescript/vfs"; import * as path from "node:path"; import dedent from "dedent"; -import { buildGenerator } from "./schema/types.mjs"; import { fileURLToPath } from "node:url"; import type { JSONSchema7 } from "json-schema"; +import { buildGenerator } from "./schema/types.mjs"; interface GraphSchema { state: JSONSchema7 | undefined; @@ -29,11 +29,15 @@ const INFER_TEMPLATE_PATH = path.resolve( export class SubgraphExtractor { protected program: ts.Program; + protected checker: ts.TypeChecker; + protected sourceFile: ts.SourceFile; + protected inferFile: ts.SourceFile; protected anyPregelType: ts.Type; + protected anyGraphType: ts.Type; protected strict: boolean; @@ -56,11 +60,11 @@ export class SubgraphExtractor { } private findTypeByName = (needle: string) => { - let result: ts.Type | undefined = undefined; + let result: ts.Type | undefined; const visit = (node: ts.Node) => { if (ts.isTypeAliasDeclaration(node)) { - const symbol = (node as any).symbol; + const {symbol} = (node as any); if (symbol != null) { const name = this.checker @@ -81,7 +85,7 @@ export class SubgraphExtractor { root: ts.Node, predicate: (node: ts.Node) => boolean ): ts.Node | undefined => { - let result: ts.Node | undefined = undefined; + let result: ts.Node | undefined; const visit = (node: ts.Node) => { if (predicate(node)) { @@ -158,7 +162,7 @@ export class SubgraphExtractor { } acc.push({ - namespace: namespace, + namespace, node: nodeName, subgraph: variables[0], }); @@ -585,10 +589,10 @@ export class SubgraphExtractor { const allDiagnostics = ts.getPreEmitDiagnostics(extract); for (const diagnostic of allDiagnostics) { let message = - ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") + "\n"; + `${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n") }\n`; if (diagnostic.file) { - const fileName = diagnostic.file.fileName; + const {fileName} = diagnostic.file; const { line, character } = ts.getLineAndCharacterOfPosition( diagnostic.file, diagnostic.start! @@ -602,7 +606,7 @@ export class SubgraphExtractor { const schemaGenerator = buildGenerator(extract); const trySymbol = (symbol: string) => { - let schema: JSONSchema7 | undefined = undefined; + let schema: JSONSchema7 | undefined; try { schema = schemaGenerator?.getSchemaForSymbol(symbol) ?? undefined; } catch (e) { @@ -614,7 +618,7 @@ export class SubgraphExtractor { if (schema == null) return undefined; - const definitions = schema.definitions; + const {definitions} = schema; if (definitions == null) return schema; const toReplace = Object.keys(definitions).flatMap((key) => { diff --git a/libs/langgraph-api/src/graph/parser/schema/types.mts b/libs/langgraph-api/src/graph/parser/schema/types.mts index b9cef53e3..9eaedec01 100644 --- a/libs/langgraph-api/src/graph/parser/schema/types.mts +++ b/libs/langgraph-api/src/graph/parser/schema/types.mts @@ -200,7 +200,7 @@ function extend(target: any, ..._: any[]): any { const to = Object(target); - for (var index = 1; index < arguments.length; index++) { + for (let index = 1; index < arguments.length; index++) { const nextSource = arguments[index]; if (nextSource != null) { @@ -372,7 +372,7 @@ function makeNullable(def: Definition): Definition { union.push({ type: "null" }); } else { const subdef: DefinitionIndex = {}; - for (var k in def as any) { + for (const k in def as any) { if (def.hasOwnProperty(k)) { subdef[k] = def[k as keyof Definition]; delete def[k as keyof typeof def]; @@ -503,16 +503,19 @@ class JsonSchemaGenerator { * information. */ private symbols: SymbolRef[]; + /** * All types for declarations of classes, interfaces, enums, and type aliases * defined in all TS files. */ private allSymbols: { [name: string]: ts.Type }; + /** * All symbols for declarations of classes, interfaces, enums, and type aliases * defined in non-default-lib TS files. */ private userSymbols: { [name: string]: ts.Symbol }; + /** * Maps from the names of base types to the names of the types that inherit from * them. @@ -547,6 +550,7 @@ class JsonSchemaGenerator { * map from type IDs to type names. */ private typeNamesById: { [id: number]: string } = {}; + /** * Whenever a type is assigned its name, its entry in this dictionary is set, * so that we don't give the same name to two separate types. @@ -640,7 +644,7 @@ class JsonSchemaGenerator { const jsdocs = symbol.getJsDocTags(); jsdocs.forEach((doc) => { // if we have @TJS-... annotations, we have to parse them - let name = doc.name; + let {name} = doc; const originalText = doc.text ? doc.text.map((t) => t.text).join("") : ""; let text = originalText; // In TypeScript versions prior to 3.7, it stops parsing the annotation @@ -654,7 +658,7 @@ class JsonSchemaGenerator { text = "true"; } } else if (name === "TJS" && text.startsWith("-")) { - let match: string[] | RegExpExecArray | null = new RegExp( + const match: string[] | RegExpExecArray | null = new RegExp( REGEX_TJS_JSDOC ).exec(originalText); if (match) { @@ -743,7 +747,7 @@ class JsonSchemaGenerator { undefined, ts.TypeFormatFlags.UseFullyQualifiedType ); - const flags = propertyType.flags; + const {flags} = propertyType; const arrayType = this.tc.getIndexTypeOfType( propertyType, ts.IndexKind.Number @@ -823,7 +827,7 @@ class JsonSchemaGenerator { [NUMERIC_INDEX_PATTERN]: this.getTypeDefinition(arrayType), }; if ( - !!Array.from((propertyType as any).members as any[])?.find( + Array.from((propertyType as any).members as any[])?.find( (member: [string]) => member[0] !== "__index" ) ) { @@ -877,7 +881,7 @@ class JsonSchemaGenerator { } else { // Report that type could not be processed const error = new TypeError( - "Unsupported type: " + propertyTypeString + `Unsupported type: ${ propertyTypeString}` ); (error as any).type = propertyType; throw error; @@ -943,7 +947,7 @@ class JsonSchemaGenerator { if ((initial as any).expression) { // node - console.warn("initializer is expression for property " + propertyName); + console.warn(`initializer is expression for property ${ propertyName}`); } else if ( (initial as any).kind && (initial as any).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral @@ -952,7 +956,7 @@ class JsonSchemaGenerator { } else { try { const sandbox = { sandboxvar: null as any }; - vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox); + vm.runInNewContext(`sandboxvar=${ initial.getText()}`, sandbox); const val = sandbox.sandboxvar; if ( @@ -965,12 +969,12 @@ class JsonSchemaGenerator { definition.default = val; } else if (val) { console.warn( - "unknown initializer for property " + propertyName + ": " + val + `unknown initializer for property ${ propertyName }: ${ val}` ); } } catch (e) { console.warn( - "exception evaluating initializer for property " + propertyName + `exception evaluating initializer for property ${ propertyName}` ); } } @@ -993,7 +997,7 @@ class JsonSchemaGenerator { node.kind === ts.SyntaxKind.EnumDeclaration ? (node as ts.EnumDeclaration).members : ts.factory.createNodeArray([node as ts.EnumMember]); - var enumValues: (number | boolean | string | null)[] = []; + const enumValues: (number | boolean | string | null)[] = []; const enumTypes: JSONSchema7TypeName[] = []; const addType = (type: JSONSchema7TypeName) => { @@ -1015,7 +1019,7 @@ class JsonSchemaGenerator { if ((initial as any).expression) { // node const exp = (initial as any).expression; - const text = (exp as any).text; + const {text} = (exp as any); // if it is an expression with a text literal, chances are it is the enum convention: // CASELABEL = 'literal' as any if (text) { @@ -1029,10 +1033,10 @@ class JsonSchemaGenerator { addType("boolean"); } else { console.warn( - "initializer is expression for enum: " + - fullName + - "." + - caseLabel + `initializer is expression for enum: ${ + fullName + }.${ + caseLabel}` ); } } else if ( @@ -1469,7 +1473,7 @@ class JsonSchemaGenerator { this.typeIdsByName[name] !== undefined && this.typeIdsByName[name] !== id; ++i ) { - name = baseName + "_" + i; + name = `${baseName }_${ i}`; } this.typeNamesById[id] = name; @@ -1618,7 +1622,7 @@ class JsonSchemaGenerator { // We don't return the full definition, but we put it into // reffedDefinitions below. returnedDefinition = { - $ref: `${this.args.id}#/definitions/` + fullTypeName, + $ref: `${this.args.id}#/definitions/${ fullTypeName}`, }; } @@ -1700,7 +1704,7 @@ class JsonSchemaGenerator { definition.additionalProperties = false; } - const types = (typ as ts.IntersectionType).types; + const {types} = (typ as ts.IntersectionType); for (const member of types) { const other = this.getTypeDefinition( member, @@ -1784,13 +1788,13 @@ class JsonSchemaGenerator { }, {}); returnedDefinition = { - $ref: `${this.args.id}#/definitions/` + fullTypeName, + $ref: `${this.args.id}#/definitions/${ fullTypeName}`, ...annotations, }; } } - if (otherAnnotations["nullable"]) { + if (otherAnnotations.nullable) { makeNullable(returnedDefinition); } @@ -1817,9 +1821,7 @@ class JsonSchemaGenerator { if (overrideDefinition) { def = { ...overrideDefinition }; } else { - def = overrideDefinition - ? overrideDefinition - : this.getTypeDefinition( + def = overrideDefinition || this.getTypeDefinition( this.allSymbols[symbolName], this.args.topRef, undefined, @@ -1836,10 +1838,10 @@ class JsonSchemaGenerator { ) { def.definitions = this.reffedDefinitions; } - def["$schema"] = "http://json-schema.org/draft-07/schema#"; - const id = this.args.id; + def.$schema = "http://json-schema.org/draft-07/schema#"; + const {id} = this.args; if (id) { - def["$id"] = this.args.id; + def.$id = this.args.id; } return def; } @@ -1860,10 +1862,10 @@ class JsonSchemaGenerator { this.resetSchemaSpecificProperties(includeAllOverrides); - const id = this.args.id; + const {id} = this.args; if (id) { - root["$id"] = id; + root.$id = id; } for (const symbolName of symbolNames) { @@ -1969,7 +1971,7 @@ export function buildGenerator( node.kind === ts.SyntaxKind.EnumDeclaration || node.kind === ts.SyntaxKind.TypeAliasDeclaration ) { - const symbol: ts.Symbol = (node as any).symbol; + const {symbol} = (node as any); const nodeType = tc.getTypeAtLocation(node); const fullyQualifiedName = tc.getFullyQualifiedName(symbol); const typeName = fullyQualifiedName.replace(/".*"\./, ""); @@ -1985,7 +1987,7 @@ export function buildGenerator( const baseTypes = nodeType.getBaseTypes() || []; baseTypes.forEach((baseType) => { - var baseName = tc.typeToString( + const baseName = tc.typeToString( baseType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType diff --git a/libs/langgraph-api/src/logging.mts b/libs/langgraph-api/src/logging.mts index 38e03a82c..6f883a7c5 100644 --- a/libs/langgraph-api/src/logging.mts +++ b/libs/langgraph-api/src/logging.mts @@ -113,11 +113,11 @@ const formatStack = (stack: string | undefined | null) => { codeFrame = codeFrame .split("\n") - .map((i) => padding + i + "\x1b[0m") + .map((i) => `${padding + i }\x1b[0m`) .join("\n"); if (highlightCode) { - codeFrame = "\x1b[36m" + codeFrame + "\x1b[31m"; + codeFrame = `\x1b[36m${ codeFrame }\x1b[31m`; } // insert codeframe after the line but dont lose the stack diff --git a/libs/langgraph-api/src/queue.mts b/libs/langgraph-api/src/queue.mts index 9c1b67356..52542de50 100644 --- a/libs/langgraph-api/src/queue.mts +++ b/libs/langgraph-api/src/queue.mts @@ -25,12 +25,12 @@ export const queue = async () => { const worker = async (run: Run, attempt: number, signal: AbortSignal) => { const startedAt = new Date(); - let endedAt: Date | undefined = undefined; - let checkpoint: StreamCheckpoint | undefined = undefined; - let exception: Error | undefined = undefined; - let status: RunStatus | undefined = undefined; + let endedAt: Date | undefined; + let checkpoint: StreamCheckpoint | undefined; + let exception: Error | undefined; + let status: RunStatus | undefined; - const temporary = run.kwargs.temporary; + const {temporary} = run.kwargs; const webhook = run.kwargs.webhook as string | undefined; logger.info("Starting background run", { diff --git a/libs/langgraph-api/src/server.mts b/libs/langgraph-api/src/server.mts index 572737785..30c131a14 100644 --- a/libs/langgraph-api/src/server.mts +++ b/libs/langgraph-api/src/server.mts @@ -1,6 +1,9 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; +import { zValidator } from "@hono/zod-validator"; +import { z } from "zod"; +import { getConfig } from "@langchain/langgraph"; import { registerFromEnv } from "./graph/load.mjs"; import runs from "./api/runs.mjs"; @@ -10,8 +13,6 @@ import store from "./api/store.mjs"; import meta from "./api/meta.mjs"; import { truncate, conn as opsConn } from "./storage/ops.mjs"; -import { zValidator } from "@hono/zod-validator"; -import { z } from "zod"; import { queue } from "./queue.mjs"; import { logger, @@ -27,7 +28,6 @@ import { registerHttp } from "./http/custom.mjs"; import { cors, ensureContentType } from "./http/middleware.mjs"; import { bindLoopbackFetch } from "./loopback.mjs"; import { checkLangGraphSemver } from "./semver/index.mjs"; -import { getConfig } from "@langchain/langgraph"; export const StartServerSchema = z.object({ port: z.number(), @@ -126,7 +126,7 @@ export async function startServer(options: z.infer) { const config = getConfig(); if (config == null) return info; - const node = config.metadata?.["langgraph_node"]; + const node = config.metadata?.langgraph_node; if (node != null) info.langgraph_node = node; return info; diff --git a/libs/langgraph-api/src/storage/ops.mts b/libs/langgraph-api/src/storage/ops.mts index 37338b230..396ba6168 100644 --- a/libs/langgraph-api/src/storage/ops.mts +++ b/libs/langgraph-api/src/storage/ops.mts @@ -149,9 +149,11 @@ interface Message { class Queue { private log: Message[] = []; + private listeners: ((idx: number) => void)[] = []; private nextId = 0; + private resumable = false; constructor(options: { resumable: boolean }) { @@ -170,7 +172,7 @@ class Queue { signal?: AbortSignal; }): Promise<[id: string, message: Message]> { if (this.resumable) { - const lastEventId = options.lastEventId; + const {lastEventId} = options; // Generator stores internal state of the read head index, let targetId = lastEventId != null ? +lastEventId + 1 : null; @@ -192,8 +194,8 @@ class Queue { } } - let timeout: NodeJS.Timeout | undefined = undefined; - let resolver: ((idx: number) => void) | undefined = undefined; + let timeout: NodeJS.Timeout | undefined; + let resolver: ((idx: number) => void) | undefined; const clean = new AbortController(); @@ -235,6 +237,7 @@ class CancellationAbortController extends AbortController { class StreamManagerImpl { readers: Record = {}; + control: Record = {}; getQueue( @@ -337,31 +340,31 @@ export class Assistants { }); yield* conn.withGenerator(async function* (STORE) { - let filtered = Object.values(STORE.assistants) + const filtered = Object.values(STORE.assistants) .filter((assistant) => { if ( options.graph_id != null && - assistant["graph_id"] !== options.graph_id + assistant.graph_id !== options.graph_id ) { return false; } if ( options.metadata != null && - !isJsonbContained(assistant["metadata"], options.metadata) + !isJsonbContained(assistant.metadata, options.metadata) ) { return false; } - if (!isAuthMatching(assistant["metadata"], filters)) { + if (!isAuthMatching(assistant.metadata, filters)) { return false; } return true; }) .sort((a, b) => { - const aCreatedAt = a["created_at"]?.getTime() ?? 0; - const bCreatedAt = b["created_at"]?.getTime() ?? 0; + const aCreatedAt = a.created_at?.getTime() ?? 0; + const bCreatedAt = b.created_at?.getTime() ?? 0; return bCreatedAt - aCreatedAt; }); @@ -395,7 +398,7 @@ export class Assistants { const result = STORE.assistants[assistant_id]; if (result == null) throw new HTTPException(404, { message: "Assistant not found" }); - if (!isAuthMatching(result["metadata"], filters)) { + if (!isAuthMatching(result.metadata, filters)) { throw new HTTPException(404, { message: "Assistant not found" }); } return { ...result, name: result.name ?? result.graph_id }; @@ -446,7 +449,7 @@ export class Assistants { const now = new Date(); STORE.assistants[assistant_id] ??= { - assistant_id: assistant_id, + assistant_id, version: 1, config: options.config ?? {}, context: options.context ?? {}, @@ -458,7 +461,7 @@ export class Assistants { }; STORE.assistant_versions.push({ - assistant_id: assistant_id, + assistant_id, version: 1, graph_id: options.graph_id, config: options.config ?? {}, @@ -501,7 +504,7 @@ export class Assistants { throw new HTTPException(404, { message: "Assistant not found" }); } - if (!isAuthMatching(assistant["metadata"], filters)) { + if (!isAuthMatching(assistant.metadata, filters)) { throw new HTTPException(404, { message: "Assistant not found" }); } @@ -510,38 +513,38 @@ export class Assistants { const metadata = mutable.metadata != null ? { - ...assistant["metadata"], + ...assistant.metadata, ...mutable.metadata, } : null; if (options?.graph_id != null) { - assistant["graph_id"] = options?.graph_id ?? assistant["graph_id"]; + assistant.graph_id = options?.graph_id ?? assistant.graph_id; } if (options?.config != null) { - assistant["config"] = options?.config ?? assistant["config"]; + assistant.config = options?.config ?? assistant.config; } if (options?.context != null) { - assistant["context"] = options?.context ?? assistant["context"]; + assistant.context = options?.context ?? assistant.context; } if (options?.name != null) { - assistant["name"] = options?.name ?? assistant["name"]; + assistant.name = options?.name ?? assistant.name; } if (metadata != null) { - assistant["metadata"] = metadata ?? assistant["metadata"]; + assistant.metadata = metadata ?? assistant.metadata; } - assistant["updated_at"] = now; + assistant.updated_at = now; const newVersion = Math.max( ...STORE.assistant_versions - .filter((v) => v["assistant_id"] === assistantId) - .map((v) => v["version"]) + .filter((v) => v.assistant_id === assistantId) + .map((v) => v.version) ) + 1; assistant.version = newVersion; @@ -549,11 +552,11 @@ export class Assistants { const newVersionEntry = { assistant_id: assistantId, version: newVersion, - graph_id: options?.graph_id ?? assistant["graph_id"], - config: options?.config ?? assistant["config"], - context: options?.context ?? assistant["context"], - name: options?.name ?? assistant["name"], - metadata: metadata ?? assistant["metadata"], + graph_id: options?.graph_id ?? assistant.graph_id, + config: options?.config ?? assistant.config, + context: options?.context ?? assistant.context, + name: options?.name ?? assistant.name, + metadata: metadata ?? assistant.metadata, created_at: now, }; @@ -576,7 +579,7 @@ export class Assistants { throw new HTTPException(404, { message: "Assistant not found" }); } - if (!isAuthMatching(assistant["metadata"], filters)) { + if (!isAuthMatching(assistant.metadata, filters)) { throw new HTTPException(404, { message: "Assistant not found" }); } @@ -584,12 +587,12 @@ export class Assistants { // Cascade delete for assistant versions and crons STORE.assistant_versions = STORE.assistant_versions.filter( - (v) => v["assistant_id"] !== assistant_id + (v) => v.assistant_id !== assistant_id ); for (const run of Object.values(STORE.runs)) { - if (run["assistant_id"] === assistant_id) { - delete STORE.runs[run["run_id"]]; + if (run.assistant_id === assistant_id) { + delete STORE.runs[run.run_id]; } } @@ -613,12 +616,12 @@ export class Assistants { throw new HTTPException(404, { message: "Assistant not found" }); } - if (!isAuthMatching(assistant["metadata"], filters)) { + if (!isAuthMatching(assistant.metadata, filters)) { throw new HTTPException(404, { message: "Assistant not found" }); } const assistantVersion = STORE.assistant_versions.find( - (v) => v["assistant_id"] === assistant_id && v["version"] === version + (v) => v.assistant_id === assistant_id && v.version === version ); if (!assistantVersion) @@ -629,10 +632,10 @@ export class Assistants { const now = new Date(); STORE.assistants[assistant_id] = { ...assistant, - config: assistantVersion["config"], - metadata: assistantVersion["metadata"], - version: assistantVersion["version"], - name: assistantVersion["name"], + config: assistantVersion.config, + metadata: assistantVersion.metadata, + version: assistantVersion.version, + name: assistantVersion.name, updated_at: now, }; @@ -656,22 +659,22 @@ export class Assistants { return conn.with((STORE) => { const versions = STORE.assistant_versions .filter((version) => { - if (version["assistant_id"] !== assistant_id) return false; + if (version.assistant_id !== assistant_id) return false; if ( options.metadata != null && - !isJsonbContained(version["metadata"], options.metadata) + !isJsonbContained(version.metadata, options.metadata) ) { return false; } - if (!isAuthMatching(version["metadata"], filters)) { + if (!isAuthMatching(version.metadata, filters)) { return false; } return true; }) - .sort((a, b) => b["version"] - a["version"]); + .sort((a, b) => b.version - a.version); return versions.slice(options.offset, options.offset + options.limit); }); @@ -759,21 +762,21 @@ export class Threads { .filter((thread) => { if ( options.metadata != null && - !isJsonbContained(thread["metadata"], options.metadata) + !isJsonbContained(thread.metadata, options.metadata) ) return false; if ( options.values != null && - typeof thread["values"] !== "undefined" && - !isJsonbContained(thread["values"], options.values) + typeof thread.values !== "undefined" && + !isJsonbContained(thread.values, options.values) ) return false; - if (options.status != null && thread["status"] !== options.status) + if (options.status != null && thread.status !== options.status) return false; - if (!isAuthMatching(thread["metadata"], filters)) return false; + if (!isAuthMatching(thread.metadata, filters)) return false; return true; }) @@ -827,7 +830,7 @@ export class Threads { }); } - if (!isAuthMatching(result["metadata"], filters)) { + if (!isAuthMatching(result.metadata, filters)) { throw new HTTPException(404, { message: `Thread with ID ${thread_id} not found`, }); @@ -857,7 +860,7 @@ export class Threads { if (STORE.threads[thread_id] != null) { const existingThread = STORE.threads[thread_id]; - if (!isAuthMatching(existingThread["metadata"], filters)) { + if (!isAuthMatching(existingThread.metadata, filters)) { throw new HTTPException(409, { message: "Thread already exists" }); } @@ -869,7 +872,7 @@ export class Threads { } STORE.threads[thread_id] ??= { - thread_id: thread_id, + thread_id, created_at: now, updated_at: now, metadata: mutable?.metadata ?? {}, @@ -898,20 +901,20 @@ export class Threads { throw new HTTPException(404, { message: "Thread not found" }); } - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { // TODO: is this correct status code? throw new HTTPException(404, { message: "Thread not found" }); } const now = new Date(); if (mutable.metadata != null) { - thread["metadata"] = { - ...thread["metadata"], + thread.metadata = { + ...thread.metadata, ...mutable.metadata, }; } - thread["updated_at"] = now; + thread.updated_at = now; return thread; }); } @@ -934,7 +937,7 @@ export class Threads { } const hasPendingRuns = Object.values(STORE.runs).some( - (run) => run["thread_id"] === threadId && run["status"] === "pending" + (run) => run.thread_id === threadId && run.status === "pending" ); let status: ThreadStatus = "idle"; @@ -981,7 +984,7 @@ export class Threads { }); } - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { throw new HTTPException(404, { message: `Thread with ID ${thread_id} not found`, }); @@ -989,8 +992,8 @@ export class Threads { delete STORE.threads[thread_id]; for (const run of Object.values(STORE.runs)) { - if (run["thread_id"] === thread_id) { - delete STORE.runs[run["run_id"]]; + if (run.thread_id === thread_id) { + delete STORE.runs[run.run_id]; } } checkpointer.delete(thread_id, null); @@ -1012,7 +1015,7 @@ export class Threads { if (!thread) throw new HTTPException(409, { message: "Thread not found" }); - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { throw new HTTPException(409, { message: "Thread not found" }); } @@ -1066,9 +1069,9 @@ export class Threads { if ( result.metadata != null && "checkpoint_ns" in result.metadata && - result.metadata["checkpoint_ns"] === "" + result.metadata.checkpoint_ns === "" ) { - delete result.metadata["checkpoint_ns"]; + delete result.metadata.checkpoint_ns; } return result; } @@ -1094,7 +1097,7 @@ export class Threads { message: `Thread ${threadId} not found`, }); - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { throw new HTTPException(403); } @@ -1172,7 +1175,7 @@ export class Threads { const thread = await Threads.get(threadId, auth); - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { throw new HTTPException(403); } @@ -1237,7 +1240,7 @@ export class Threads { }); const thread = await Threads.get(threadId, auth); - if (!isAuthMatching(thread["metadata"], filters)) return []; + if (!isAuthMatching(thread.metadata, filters)) return []; const graphId = thread.metadata?.graph_id as string | undefined | null; if (graphId == null) return []; @@ -1360,7 +1363,7 @@ export class Runs { thread_id: threadId, assistant_id: assistantId, run_id: runId, - status: status, + status, metadata: options?.metadata ?? {}, prevent_insert_if_inflight: options?.preventInsertInInflight, multitask_strategy: multitaskStrategy, @@ -1379,7 +1382,7 @@ export class Runs { if ( existingThread && - !isAuthMatching(existingThread["metadata"], filters) + !isAuthMatching(existingThread.metadata, filters) ) { throw new HTTPException(404); } @@ -1396,13 +1399,11 @@ export class Runs { assistant_id: assistantId, ...metadata, }, - config: Object.assign({}, assistant.config, config, { - configurable: Object.assign( - {}, - assistant.config?.configurable, - config?.configurable - ), - }), + config: { ...assistant.config, ...config, configurable: { + + ...assistant.config?.configurable, + ...config?.configurable + },}, created_at: now, updated_at: now, }; @@ -1410,25 +1411,21 @@ export class Runs { } else if (existingThread) { if (existingThread.status !== "busy") { existingThread.status = "busy"; - existingThread.metadata = Object.assign({}, existingThread.metadata, { - graph_id: assistant.graph_id, - assistant_id: assistantId, - }); - - existingThread.config = Object.assign( - {}, - assistant.config, - existingThread.config, - config, - { - configurable: Object.assign( - {}, - assistant.config?.configurable, - existingThread?.config?.configurable, - config?.configurable - ), - } - ); + existingThread.metadata = { ...existingThread.metadata, graph_id: assistant.graph_id, + assistant_id: assistantId,}; + + existingThread.config = { + + ...assistant.config, + ...existingThread.config, + ...config, + configurable: { + + ...assistant.config?.configurable, + ...existingThread?.config?.configurable, + ...config?.configurable + }, + }; existingThread.updated_at = now; } @@ -1449,13 +1446,12 @@ export class Runs { } // create new run - const configurable = Object.assign( - {}, - assistant.config?.configurable, - existingThread?.config?.configurable, - config?.configurable, - { - run_id: runId, + const configurable = { + + ...assistant.config?.configurable, + ...existingThread?.config?.configurable, + ...config?.configurable, + run_id: runId, thread_id: threadId, graph_id: assistant.graph_id, assistant_id: assistantId, @@ -1464,35 +1460,32 @@ export class Runs { existingThread?.config?.configurable?.user_id ?? assistant.config?.configurable?.user_id ?? options?.userId, - } - ); + }; - const mergedMetadata = Object.assign( - {}, - assistant.metadata, - existingThread?.metadata, - metadata - ); + const mergedMetadata = { + + ...assistant.metadata, + ...existingThread?.metadata, + ...metadata + }; const newRun: Run = { run_id: runId, thread_id: threadId!, assistant_id: assistantId, metadata: mergedMetadata, - status: status, - kwargs: Object.assign({}, kwargs, { - config: Object.assign( - {}, - assistant.config, - config, - { configurable }, - { metadata: mergedMetadata } - ), + status, + kwargs: { ...kwargs, config: { + + ...assistant.config, + ...config, + configurable, + metadata: mergedMetadata + }, context: typeof assistant.context !== "object" && assistant.context != null ? assistant.context ?? kwargs.context - : Object.assign({}, assistant.context, kwargs.context), - }), + : ({ ...assistant.context, ...kwargs.context}),}, multitask_strategy: multitaskStrategy, created_at: new Date(now.valueOf() + afterSeconds * 1000), updated_at: now, @@ -1523,7 +1516,7 @@ export class Runs { if (filters != null) { const thread = STORE.threads[run.thread_id]; - if (!isAuthMatching(thread["metadata"], filters)) return null; + if (!isAuthMatching(thread.metadata, filters)) return null; } return run; @@ -1547,7 +1540,7 @@ export class Runs { if (filters != null) { const thread = STORE.threads[run.thread_id]; - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { throw new HTTPException(404, { message: "Run not found" }); } } @@ -1631,7 +1624,7 @@ export class Runs { if (filters != null) { const thread = STORE.threads[run.thread_id]; - if (!isAuthMatching(thread["metadata"], filters)) continue; + if (!isAuthMatching(thread.metadata, filters)) continue; } foundRunsCount += 1; @@ -1712,7 +1705,7 @@ export class Runs { if (filters != null) { const thread = STORE.threads[run.thread_id]; - if (!isAuthMatching(thread["metadata"], filters)) return false; + if (!isAuthMatching(thread.metadata, filters)) return false; } return true; }); @@ -1756,7 +1749,7 @@ export class Runs { // TODO: consolidate into a single function if (filters != null && threadId != null) { const thread = STORE.threads[threadId]; - if (!isAuthMatching(thread["metadata"], filters)) { + if (!isAuthMatching(thread.metadata, filters)) { yield { event: "error", data: { error: "Error", message: "404: Thread not found" }, diff --git a/libs/langgraph-api/src/storage/persist.mts b/libs/langgraph-api/src/storage/persist.mts index a2e634b06..81781e764 100644 --- a/libs/langgraph-api/src/storage/persist.mts +++ b/libs/langgraph-api/src/storage/persist.mts @@ -1,8 +1,8 @@ import * as path from "node:path"; import * as fs from "node:fs/promises"; import * as superjson from "superjson"; -import * as importMap from "./importMap.mjs"; import { load } from "@langchain/core/load"; +import * as importMap from "./importMap.mjs"; // Add custom transformers for Uint8Array superjson.registerCustom( @@ -25,9 +25,11 @@ export async function deserialize(input: string) { export class FileSystemPersistence { private filepath: string | null = null; + private data: Schema | null = null; private defaultSchema: () => Schema; + private name: string; private flushTimeout: NodeJS.Timeout | undefined = undefined; @@ -88,7 +90,7 @@ export class FileSystemPersistence { } let shouldPersist = false; - let schedulePersist = () => void (shouldPersist = true); + const schedulePersist = () => void (shouldPersist = true); try { const gen = diff --git a/libs/langgraph-api/src/stream.mts b/libs/langgraph-api/src/stream.mts index 7bbeecf31..a8bdc694a 100644 --- a/libs/langgraph-api/src/stream.mts +++ b/libs/langgraph-api/src/stream.mts @@ -124,14 +124,14 @@ const deleteInternalConfigurableFields = (config: unknown) => { function preprocessDebugCheckpoint(payload: DebugCheckpoint): StreamCheckpoint { const result: Record = { ...payload, - checkpoint: runnableConfigToCheckpoint(payload["config"]), - parent_checkpoint: runnableConfigToCheckpoint(payload["parentConfig"]), - tasks: payload["tasks"].map(preprocessDebugCheckpointTask), + checkpoint: runnableConfigToCheckpoint(payload.config), + parent_checkpoint: runnableConfigToCheckpoint(payload.parentConfig), + tasks: payload.tasks.map(preprocessDebugCheckpointTask), }; // Handle LangGraph JS pascalCase vs snake_case // TODO: use stream to LangGraph.JS - result.parent_config = payload["parentConfig"]; + result.parent_config = payload.parentConfig; delete result.parentConfig; result.config = deleteInternalConfigurableFields(result.config); @@ -156,7 +156,7 @@ export async function* streamState( signal?: AbortSignal; } ): AsyncGenerator<{ event: string; data: unknown }> { - const kwargs = run.kwargs; + const {kwargs} = run; const graphId = kwargs.config?.configurable?.graph_id; if (!graphId || typeof graphId !== "string") { diff --git a/libs/langgraph-api/src/utils/hono.mts b/libs/langgraph-api/src/utils/hono.mts index 0659e97fd..5b7cbf824 100644 --- a/libs/langgraph-api/src/utils/hono.mts +++ b/libs/langgraph-api/src/utils/hono.mts @@ -1,7 +1,7 @@ import type { Context } from "hono"; -import { serialiseAsDict } from "./serde.mjs"; import { stream } from "hono/streaming"; import { StreamingApi } from "hono/utils/stream"; +import { serialiseAsDict } from "./serde.mjs"; export function jsonExtra(c: Context, object: T) { c.header("Content-Type", "application/json"); diff --git a/yarn.lock b/yarn.lock index 47b6993dd..c779f0e4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1739,6 +1739,13 @@ __metadata: "@typescript/vfs": "npm:^1.6.0" dedent: "npm:^1.5.3" dotenv: "npm:^16.4.7" + dpdm: "npm:^3.12.0" + eslint: "npm:^8.33.0" + eslint-config-airbnb-base: "npm:^15.0.0" + eslint-config-prettier: "npm:^8.6.0" + eslint-plugin-import: "npm:^2.29.1" + eslint-plugin-no-instanceof: "npm:^1.0.1" + eslint-plugin-prettier: "npm:^4.2.1" exit-hook: "npm:^4.0.0" hono: "npm:^4.5.4" jose: "npm:^6.0.10"