Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn b3d1bcdd1b Log queue size when future cancelled 2025-01-10 13:51:13 -08:00
18 changed files with 88 additions and 2152 deletions
+1 -23
View File
@@ -109,31 +109,9 @@ jobs:
- name: Build
run: yarn build
test-js:
runs-on: ubuntu-latest
strategy:
matrix:
working-directory:
- "libs/sdk-js"
defaults:
run:
working-directory: ${{ matrix.working-directory }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js (LTS)
uses: actions/setup-node@v3
with:
node-version: "20"
cache: "yarn"
cache-dependency-path: ${{ matrix.working-directory }}/yarn.lock
- name: Install dependencies
run: yarn install
- name: Run tests
run: yarn test
ci_success:
name: "CI Success"
needs: [lint, lint-js, test, test-langgraph, test-scheduler-kafka, integration-test, test-js]
needs: [lint, lint-js, test, test-langgraph, test-scheduler-kafka, integration-test]
if: |
always()
runs-on: ubuntu-latest
@@ -279,58 +279,6 @@
}
}
},
"/v1/projects/{project_id}/revisions/{revision_id}/deploy": {
"post": {
"tags": ["Revisions (v1)"],
"summary": "Deploy Revision",
"description": "Deploy revision by ID.\n\nThis endpoint redeploys the deployment of a revision without rebuilding the image for the deployment. Redeploying the deployment of a revision may mitigate intermittent issues with a deployment.\n\nThe revision must be in the `DEPLOYED` status and must be the latest revision of the project.",
"operationId": "deploy_revision_projects__project_id__revisions__revision_id__deploy_post",
"parameters": [
{
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Project ID"
},
"name": "project_id",
"in": "path"
},
{
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Revision ID"
},
"name": "revision_id",
"in": "path"
}
],
"responses": {
"400": {
"description": "Revision is not in DEPLOYED status or revision is not the latest revision for the project.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Revision not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/v1/projects/{project_id}/revisions/{revision_id}/interrupt": {
"post": {
"tags": ["Revisions (v1)"],
@@ -371,6 +319,31 @@
}
},
"schemas": {
"EnvVar": {
"type": "object",
"description": "An environment variable or secret.",
"properties": {
"name": {
"type": "string",
"description": "Environment variable or secret name.",
"required": true
},
"value": {
"type": "string",
"description": "Environment variable or secret value.",
"required": true
},
"type": {
"type": "string",
"enum": [
"default",
"secret"
],
"description": "Field to designate type of the environment variable (default) or secret.",
"required": true
}
}
},
"ContainerSpec": {
"type": "object",
"description": "Container specification for a revision's deployment.\n\nIf any field is omitted or set to `null`, the internal default value is used depending on the deployment type (`dev` or `prod`).",
@@ -431,42 +404,6 @@
}
}
},
"EnvVar": {
"type": "object",
"description": "An environment variable or secret.",
"properties": {
"name": {
"type": "string",
"description": "Environment variable or secret name.",
"required": true
},
"value": {
"type": "string",
"description": "Environment variable or secret value.",
"required": true
},
"type": {
"type": "string",
"enum": [
"default",
"secret"
],
"description": "Field to designate type of the environment variable (default) or secret.",
"required": true
}
}
},
"ErrorResponse": {
"type": "object",
"description": "Error response.",
"properties": {
"detail": {
"type": "string",
"description": "Error details.",
"required": true
}
}
},
"Project": {
"type": "object",
"description": "A project corresponds to a LangGraph Server deployment and the associated LangSmith tracing project.",
+1 -4
View File
@@ -289,7 +289,4 @@ RUN set -ex && \
RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}'
```
???+ note "Updating your langgraph.json file"
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
```
-3
View File
@@ -62,9 +62,6 @@ The server includes all API endpoints for your graph's runs, threads, assistants
The `langgraph dockerfile` command generates a [Dockerfile](https://docs.docker.com/reference/dockerfile/) that can be used to build images for and deploy instances of the [LangGraph API server](./langgraph_server.md). This is useful if you want to further customize the dockerfile or deploy in a more custom way.
??? note "Updating your langgraph.json file"
The `langgraph dockerfile` command translates all the configuration in your `langgraph.json` file into Dockerfile commands. When using this command, you will have to re-run it whenever you update your `langgraph.json` file. Otherwise, your changes will not be reflected when you build or run the dockerfile.
## Related
- [LangGraph CLI API Reference](../cloud/reference/cli.md)
-1
View File
@@ -29,7 +29,6 @@ You will eventually need to pass in the following environment variables to the L
- `DATABASE_URI`: Postgres connection details. Postgres will be used to store assistants, threads, runs, persist thread state and long term memory, and to manage the state of the background task queue with 'exactly once' semantics.
- `LANGSMITH_API_KEY`: (If using [Self-Hosted Lite](../concepts/deployment_options.md#self-hosted-lite)) LangSmith API key. This will be used to authenticate ONCE at server start up.
- `LANGGRAPH_CLOUD_LICENSE_KEY`: (If using [Self-Hosted Enterprise](../concepts/deployment_options.md#self-hosted-enterprise)) LangGraph Platform license key. This will be used to authenticate ONCE at server start up.
- `LANGCHAIN_ENDPOINT`: To send traces to a [self-hosted LangSmith](https://docs.smith.langchain.com/self_hosting) instance, set `LANGCHAIN_ENDPOINT` to the hostname of the self-hosted LangSmith instance.
## Build the Docker Image
@@ -208,7 +208,7 @@
"from typing import Optional\n",
"\n",
"from langchain.chat_models import init_chat_model\n",
"from langgraph.prebuilt import InjectedStore\n",
"from langchain_core.tools import InjectedToolArg\n",
"from langgraph.store.base import BaseStore\n",
"from typing_extensions import Annotated\n",
"\n",
@@ -232,7 +232,7 @@
" content: str,\n",
" *,\n",
" memory_id: Optional[uuid.UUID] = None,\n",
" store: Annotated[BaseStore, InjectedStore],\n",
" store: Annotated[BaseStore, InjectedToolArg],\n",
"):\n",
" \"\"\"Upsert a memory in the database.\"\"\"\n",
" # The LLM can use this tool to store a new memory\n",
@@ -246,7 +246,7 @@
"\n",
"Define the (`fetch_user_flight_information`) tool to let the agent see the current user's flight information. Then define tools to search for flights and manage the passenger's bookings stored in the SQL database.\n",
"\n",
"We then can [access the RunnableConfig](https://python.langchain.com/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n",
"We the can [access the RunnableConfig](https://python.langchain.com/docs/how_to/tool_configure/#inferring-by-parameter-type) for a given run to check the `passenger_id` of the user accessing this application. The LLM never has to provide these explicitly, they are provided for a given invocation of the graph so that each user cannot access other passengers' booking information.\n",
"\n",
"<div class=\"admonition warning\">\n",
" <p class=\"admonition-title\">Compatibility</p>\n",
+42 -6
View File
@@ -1,7 +1,18 @@
import asyncio
import functools
import weakref
from typing import Any, Callable, Iterable, Literal, Optional, TypeVar, Union
import sys
from typing import (
Any,
Callable,
Iterable,
Literal,
Optional,
TypeVar,
Union,
AsyncIterator,
)
from contextlib import asynccontextmanager
from langgraph.store.base import (
BaseStore,
@@ -17,9 +28,15 @@ from langgraph.store.base import (
SearchOp,
_validate_namespace,
)
import asyncio
import logging
logger = logging.getLogger(__name__)
F = TypeVar("F", bound=Callable)
SUPPORTS_EXC_NOTES = sys.version_info >= (3, 11)
def _check_loop(func: F) -> F:
@functools.wraps(func)
@@ -67,7 +84,8 @@ class AsyncBatchedBaseStore(BaseStore):
) -> Optional[Item]:
fut = self._loop.create_future()
self._aqueue[fut] = GetOp(namespace, key)
return await fut
async with _raise_with_queue_stats(self._aqueue):
return await fut
async def asearch(
self,
@@ -81,7 +99,8 @@ class AsyncBatchedBaseStore(BaseStore):
) -> list[SearchItem]:
fut = self._loop.create_future()
self._aqueue[fut] = SearchOp(namespace_prefix, filter, limit, offset, query)
return await fut
async with _raise_with_queue_stats(self._aqueue):
return await fut
async def aput(
self,
@@ -93,7 +112,8 @@ class AsyncBatchedBaseStore(BaseStore):
_validate_namespace(namespace)
fut = self._loop.create_future()
self._aqueue[fut] = PutOp(namespace, key, value, index)
return await fut
async with _raise_with_queue_stats(self._aqueue):
return await fut
async def adelete(
self,
@@ -102,7 +122,8 @@ class AsyncBatchedBaseStore(BaseStore):
) -> None:
fut = self._loop.create_future()
self._aqueue[fut] = PutOp(namespace, key, None)
return await fut
async with _raise_with_queue_stats(self._aqueue):
return await fut
async def alist_namespaces(
self,
@@ -127,7 +148,8 @@ class AsyncBatchedBaseStore(BaseStore):
offset=offset,
)
self._aqueue[fut] = op
return await fut
async with _raise_with_queue_stats(self._aqueue):
return await fut
@_check_loop
def batch(self, ops: Iterable[Op]) -> list[Result]:
@@ -281,3 +303,17 @@ async def _run(
break
# remove strong ref to store
del s
@asynccontextmanager
async def _raise_with_queue_stats(
queue: dict[asyncio.Future, Op]
) -> AsyncIterator[None]:
try:
yield
except asyncio.CancelledError as e:
if SUPPORTS_EXC_NOTES:
e.add_note(f"Queue size: {len(queue)}")
else:
logger.warning(f"Queue size: {len(queue)}")
raise
+2 -2
View File
@@ -100,7 +100,7 @@ class Config(TypedDict, total=False):
def _parse_version(version_str: str) -> tuple[int, int]:
"""Parse a version string into a tuple of (major, minor)."""
try:
major, minor = map(int, version_str.split("-")[0].split("."))
major, minor = map(int, version_str.split("."))
return (major, minor)
except ValueError:
raise click.UsageError(f"Invalid version format: {version_str}") from None
@@ -159,7 +159,7 @@ def validate_config(config: Config) -> Config:
if config.get("python_version"):
pyversion = config["python_version"]
if not pyversion.count(".") == 1 or not all(
part.isdigit() for part in pyversion.split("-")[0].split(".")
part.isdigit() for part in pyversion.split(".")
):
raise click.UsageError(
f"Invalid Python version format: {pyversion}. "
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "langgraph-cli"
version = "0.1.66"
version = "0.1.65"
description = "CLI for interacting with LangGraph API"
authors = []
license = "MIT"
-6
View File
@@ -91,12 +91,6 @@ def test_validate_config():
validate_config({"python_version": "3.10"})
assert "Minimum required version" in str(exc_info.value)
config = validate_config({"python_version": "3.11-bullseye", "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}})
assert config["python_version"] == "3.11-bullseye"
config = validate_config({"python_version": "3.12-slim", "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}})
assert config["python_version"] == "3.12-slim"
def test_validate_config_file():
with tempfile.TemporaryDirectory() as tmpdir:
-17
View File
@@ -1,17 +0,0 @@
/** @type {import('jest').Config} */
export default {
preset: 'ts-jest',
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
useESM: true,
},
],
},
};
+2 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@langchain/langgraph-sdk",
"version": "0.0.36",
"version": "0.0.35",
"description": "Client library for interacting with the LangGraph API",
"type": "module",
"packageManager": "yarn@1.22.19",
@@ -9,8 +9,7 @@
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
"prepublish": "yarn run build",
"format": "prettier --write src",
"lint": "prettier --check src && tsc --noEmit",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts"
"lint": "prettier --check src && tsc --noEmit"
},
"main": "index.js",
"license": "MIT",
@@ -21,16 +20,12 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@langchain/scripts": "^0.1.4",
"@tsconfig/recommended": "^1.0.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.12",
"@types/uuid": "^9.0.1",
"concat-md": "^0.5.1",
"jest": "^29.7.0",
"prettier": "^3.2.5",
"ts-jest": "^29.1.2",
"typedoc": "^0.26.1",
"typedoc-plugin-markdown": "^4.1.0",
"typescript": "^5.4.5"
+4 -7
View File
@@ -37,7 +37,7 @@ import {
} from "./types.js";
import { mergeSignals } from "./utils/signals.js";
import { getEnvironmentVariable } from "./utils/env.js";
import { _getFetchImplementation } from "./singletons/fetch.js";
/**
* Get the API key from the environment.
* Precedence:
@@ -164,8 +164,7 @@ class BaseClient {
signal?: AbortSignal;
},
): Promise<T> {
const response = await this.asyncCaller.call(
_getFetchImplementation(),
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(path, options),
);
if (response.status === 202 || response.status === 204) {
@@ -753,8 +752,7 @@ export class RunsClient extends BaseClient {
const endpoint =
threadId == null ? `/runs/stream` : `/threads/${threadId}/runs/stream`;
const response = await this.asyncCaller.call(
_getFetchImplementation(),
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(endpoint, {
method: "POST",
json,
@@ -1046,8 +1044,7 @@ export class RunsClient extends BaseClient {
? { signal: options }
: options;
const response = await this.asyncCaller.call(
_getFetchImplementation(),
const response = await this.asyncCaller.fetch(
...this.prepareFetchOptions(`/threads/${threadId}/runs/${runId}/stream`, {
method: "GET",
timeoutMs: null,
-1
View File
@@ -17,6 +17,5 @@ export type {
Checkpoint,
Interrupt,
} from "./schema.js";
export { overrideFetchImplementation } from "./singletons/fetch.js";
export type { OnConflictBehavior, Command } from "./types.js";
-29
View File
@@ -1,29 +0,0 @@
// Wrap the default fetch call due to issues with illegal invocations
// in some environments:
// https://stackoverflow.com/questions/69876859/why-does-bind-fix-failed-to-execute-fetch-on-window-illegal-invocation-err
// @ts-expect-error Broad typing to support a range of fetch implementations
const DEFAULT_FETCH_IMPLEMENTATION = (...args: any[]) => fetch(...args);
const LANGSMITH_FETCH_IMPLEMENTATION_KEY = Symbol.for(
"lg:fetch_implementation",
);
/**
* Overrides the fetch implementation used for LangSmith calls.
* You should use this if you need to use an implementation of fetch
* other than the default global (e.g. for dealing with proxies).
* @param fetch The new fetch function to use.
*/
export const overrideFetchImplementation = (fetch: (...args: any[]) => any) => {
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] = fetch;
};
/**
* @internal
*/
export const _getFetchImplementation: () => (...args: any[]) => any = () => {
return (
(globalThis as any)[LANGSMITH_FETCH_IMPLEMENTATION_KEY] ??
DEFAULT_FETCH_IMPLEMENTATION
);
};
-74
View File
@@ -1,74 +0,0 @@
/* eslint-disable no-process-env */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { jest } from "@jest/globals";
import { Client } from "../client.js";
import { overrideFetchImplementation } from "../singletons/fetch.js";
describe.each([[""], ["mocked"]])("Client uses %s fetch", (description) => {
let globalFetchMock: jest.Mock;
let overriddenFetch: jest.Mock;
let expectedFetchMock: jest.Mock;
let unexpectedFetchMock: jest.Mock;
beforeEach(() => {
globalFetchMock = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
batch_ingest_config: {
use_multipart_endpoint: true,
},
}),
text: () => Promise.resolve(""),
}),
);
overriddenFetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
batch_ingest_config: {
use_multipart_endpoint: true,
},
}),
text: () => Promise.resolve(""),
}),
);
expectedFetchMock =
description === "mocked" ? overriddenFetch : globalFetchMock;
unexpectedFetchMock =
description === "mocked" ? globalFetchMock : overriddenFetch;
if (description === "mocked") {
overrideFetchImplementation(overriddenFetch);
} else {
overrideFetchImplementation(globalFetchMock);
}
// Mock global fetch
(globalThis as any).fetch = globalFetchMock;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe("createRuns", () => {
it("should create an example with the given input and generation", async () => {
const client = new Client({ apiKey: "test-api-key" });
const thread = await client.threads.create();
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
expect(unexpectedFetchMock).not.toHaveBeenCalled();
jest.clearAllMocks(); // Clear all mocks before the next operation
// Then clear & run the function
await client.runs.create(thread.thread_id, "somegraph", {
input: { foo: "bar" },
});
expect(expectedFetchMock).toHaveBeenCalledTimes(1);
expect(unexpectedFetchMock).not.toHaveBeenCalled();
});
});
});
+7 -1880
View File
File diff suppressed because it is too large Load Diff