diff --git a/.github/scripts/run_langgraph_cli_test.py b/.github/scripts/run_langgraph_cli_test.py index 478a215ab..872b86f65 100644 --- a/.github/scripts/run_langgraph_cli_test.py +++ b/.github/scripts/run_langgraph_cli_test.py @@ -1,107 +1,145 @@ -import asyncio -import json -import os import pathlib import sys -import langgraph_cli -import langgraph_cli.docker -import langgraph_cli.config +import time +from urllib import request, error +import langgraph_cli +import langgraph_cli.config +import langgraph_cli.docker +from langgraph_cli.cli import prepare_args_and_stdin +from langgraph_cli.constants import DEFAULT_PORT from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.progress import Progress -from langgraph_cli.constants import DEFAULT_PORT -def test( - config: pathlib.Path, - port: int, - tag: str, - verbose: bool, -): +def test(config: pathlib.Path, port: int, tag: str, verbose: bool): + """Spin up API with Postgres/Redis via docker compose and wait until ready.""" with Runner() as runner, Progress(message="Pulling...") as set: - # check docker available + # Detect docker/compose capabilities capabilities = langgraph_cli.docker.check_capabilities(runner) - # open config + + # Validate config and prepare compose stdin/args using built image config_json = langgraph_cli.config.validate_config_file(config) + args, stdin = prepare_args_and_stdin( + capabilities=capabilities, + config_path=config, + config=config_json, + docker_compose=None, + port=port, + watch=False, + debugger_port=None, + debugger_base_url=f"http://127.0.0.1:{port}", + postgres_uri=None, + api_version=None, + image=tag, + base_image=None, + ) - set("Running...") - args = [ - "run", - "--rm", - "-p", - f"{port}:8000", - ] - if isinstance(config_json["env"], str): - args.extend( - [ - "--env-file", - str(config.parent / config_json["env"]), - ] - ) - else: - for k, v in config_json["env"].items(): - args.extend( - [ - "-e", - f"{k}={v}", - ] - ) - if capabilities.healthcheck_start_interval: - args.extend( - [ - "--health-interval", - "5s", - "--health-retries", - "1", - "--health-start-period", - "10s", - "--health-start-interval", - "1s", - ] - ) - else: - args.extend( - [ - "--health-interval", - "5s", - "--health-retries", - "2", - ] - ) + # Compose up with wait (implies detach), similar to `langgraph up --wait` + args_up = [*args, "up", "--remove-orphans", "--wait"] - _task = None - - def on_stdout(line: str): - nonlocal _task - if "GET /ok" in line or "Uvicorn running on" in line: - set("") - sys.stdout.write( - f"""Ready! -- API: http://localhost:{port} -""" - ) - sys.stdout.flush() - _task.cancel() - return True - return False - - async def subp_exec_task(*args, **kwargs): - nonlocal _task - _task = asyncio.create_task(subp_exec(*args, **kwargs)) - await _task + compose_cmd = ["docker", "compose"] + if capabilities.compose_type == "standalone": + compose_cmd = ["docker-compose"] + set("Starting...") try: runner.run( - subp_exec_task( - "docker", - *args, - tag, + subp_exec( + *compose_cmd, + *args_up, + input=stdin, verbose=verbose, - on_stdout=on_stdout, ) ) - except asyncio.CancelledError: - pass + except Exception as e: # noqa: BLE001 + # On failure, show diagnostics then ensure clean teardown + sys.stderr.write(f"docker compose up failed: {e}\n") + try: + sys.stderr.write("\n== docker compose ps ==\n") + runner.run(subp_exec(*compose_cmd, *args, "ps", input=stdin, verbose=False)) + except Exception: + pass + try: + sys.stderr.write("\n== docker compose logs (api) ==\n") + runner.run( + subp_exec( + *compose_cmd, + *args, + "logs", + "langgraph-api", + input=stdin, + verbose=False, + ) + ) + except Exception: + pass + finally: + try: + runner.run( + subp_exec( + *compose_cmd, + *args, + "down", + "-v", + "--remove-orphans", + input=stdin, + verbose=False, + ) + ) + finally: + raise + + set("") + base_url = f"http://localhost:{port}" + ok_url = f"{base_url}/ok" + print(f"Waiting for {ok_url} to respond with 200...") + deadline = time.time() + 30 + last_err: Exception | None = None + while time.time() < deadline: + try: + with request.urlopen(ok_url, timeout=2) as resp: + if resp.status == 200: + sys.stdout.write( + f"""Ready!\n- API: {base_url}\n- /ok: 200 OK\n""" + ) + sys.stdout.flush() + break + else: + last_err = RuntimeError(f"Unexpected status: {resp.status}") + print(f"Unexpected status: {resp.status}") + except error.URLError as e: + last_err = e + except Exception as e: # noqa: BLE001 + last_err = e + time.sleep(0.5) + else: + # Bring stack down before raising + args_down = [*args, "down", "-v", "--remove-orphans"] + try: + runner.run( + subp_exec( + *compose_cmd, + *args_down, + input=stdin, + verbose=verbose, + ) + ) + finally: + raise SystemExit( + f"/ok did not return 202 within timeout. Last error: {last_err}" + ) + + # Clean up: bring compose stack down to free ports for next test + args_down = [*args, "down", "-v", "--remove-orphans"] + runner.run( + subp_exec( + *compose_cmd, + *args_down, + input=stdin, + verbose=verbose, + ) + ) if __name__ == "__main__": @@ -110,6 +148,6 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("-t", "--tag", type=str) parser.add_argument("-c", "--config", type=str, default="./langgraph.json") - parser.add_argument("-p", "--port", default=DEFAULT_PORT) + parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT) args = parser.parse_args() test(pathlib.Path(args.config), args.port, args.tag, verbose=True) diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml index 5857609ec..f4aa57aa6 100644 --- a/.github/workflows/_integration_test.yml +++ b/.github/workflows/_integration_test.yml @@ -43,28 +43,43 @@ jobs: - name: Build and test service A if: steps.changed-files.outputs.all working-directory: libs/cli/examples + env: + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} run: | # The build-arg isn't used; just testing that we accept other args - langgraph build -t langgraph-test-a --base-image "langchain/langgraph-trial" - cp .env.example .envg + langgraph build -t langgraph-test-a + cp .env.example .env + if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -c langgraph.json -t langgraph-test-a - name: Build and test service B if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs + env: + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} run: | - langgraph build -t langgraph-test-b --base-image "langchain/langgraph-trial" + langgraph build -t langgraph-test-b + cp ../.env.example .env + if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-b - name: Build and test service C if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs_reqs_a + env: + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} run: | - langgraph build -t langgraph-test-c --base-image "langchain/langgraph-trial" + langgraph build -t langgraph-test-c + cp ../.env.example .env + if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-c - name: Build and test service D if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs_reqs_b + env: + LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} run: | - langgraph build -t langgraph-test-d --base-image "langchain/langgraph-trial" + langgraph build -t langgraph-test-d + cp ../.env.example .env + if [ -n "${{ secrets.LANGSMITH_API_KEY }}" ]; then echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env; fi timeout 60 python ../../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-d - name: Build JS service diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e654ee8f9..e739b064a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,9 +62,10 @@ jobs: working-directory: ${{ inputs.working-directory }} run: | PKG_NAME=$(grep -m 1 "^name = " pyproject.toml | cut -d '"' -f 2) - # sdk-py uses dynamic versioning from langgraph_sdk/__init__.py - if [ "$PKG_NAME" = "langgraph-sdk" ]; then - VERSION=$(grep -m 1 '^__version__' langgraph_sdk/__init__.py | cut -d '"' -f 2) + if grep -q 'dynamic.*=.*\[.*"version".*\]' pyproject.toml; then + # handle dynamic versioning + DIR_NAME=$(echo "$PKG_NAME" | tr '-' '_') + VERSION=$(grep -m 1 '^__version__' "${DIR_NAME}/__init__.py" | cut -d '"' -f 2) else VERSION=$(grep -m 1 "^version = " pyproject.toml | cut -d '"' -f 2) fi diff --git a/libs/cli/Makefile b/libs/cli/Makefile index 5efcc064f..0c3fcc7dc 100644 --- a/libs/cli/Makefile +++ b/libs/cli/Makefile @@ -4,8 +4,9 @@ # TESTING AND COVERAGE ###################### +TEST?= "tests/unit_tests" test: - uv run pytest tests/unit_tests + uv run pytest $(TEST) test-integration: uv run pytest tests/integration_tests diff --git a/libs/cli/examples/.env.example b/libs/cli/examples/.env.example index cc176486c..d9649108e 100644 --- a/libs/cli/examples/.env.example +++ b/libs/cli/examples/.env.example @@ -1,10 +1,3 @@ OPENAI_API_KEY=placeholder ANTHROPIC_API_KEY=placeholder TAVILY_API_KEY=placeholder -LANGCHAIN_TRACING_V2=false -LANGCHAIN_ENDPOINT=placeholder -LANGCHAIN_API_KEY=placeholder -LANGCHAIN_PROJECT=placeholder -LANGGRAPH_AUTH_TYPE=noop -LANGSMITH_AUTH_ENDPOINT=placeholder -LANGSMITH_TENANT_ID=placeholder \ No newline at end of file diff --git a/libs/cli/generate_schema.py b/libs/cli/generate_schema.py index 041012c61..ad6637a4a 100644 --- a/libs/cli/generate_schema.py +++ b/libs/cli/generate_schema.py @@ -163,14 +163,7 @@ def generate_schema(): # Add enum constraint for python_version if "python_version" in python_schema["properties"]: - python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12"] - - # Add enum constraint for image_distro - if "image_distro" in python_schema["properties"]: - python_schema["properties"]["image_distro"]["anyOf"] = [ - {"type": "string", "enum": ["debian", "wolfi"]}, - {"type": "null"}, - ] + python_schema["properties"]["python_version"]["enum"] = ["3.11", "3.12", "3.13"] # Create Node.js schema with node_version node_schema = { diff --git a/libs/cli/langgraph_cli/__init__.py b/libs/cli/langgraph_cli/__init__.py index e69de29bb..8879c6c77 100644 --- a/libs/cli/langgraph_cli/__init__.py +++ b/libs/cli/langgraph_cli/__init__.py @@ -0,0 +1 @@ +__version__ = "0.3.7" diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 43a940afc..d631bc5f1 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -17,6 +17,9 @@ DEFAULT_PYTHON_VERSION = "3.11" DEFAULT_IMAGE_DISTRO = "debian" +Distros = Literal["debian", "wolfi", "bullseye", "bookworm"] + + class TTLConfig(TypedDict, total=False): """Configuration for TTL (time-to-live) behavior in the store.""" @@ -369,6 +372,13 @@ class Config(TypedDict, total=False): Must be >= 20 if provided. """ + api_version: Optional[str] + """Optional. Which semantic version of the LangGraph API server to use. + + Defaults to latest. Check the + [changelog](https://docs.langchain.com/langgraph-platform/langgraph-server-changelog) + for more information.""" + _INTERNAL_docker_tag: Optional[str] """Optional. Internal use only. """ @@ -378,10 +388,11 @@ class Config(TypedDict, total=False): Defaults to langchain/langgraph-api or langchain/langgraphjs-api.""" - image_distro: Optional[str] + image_distro: Optional[Distros] """Optional. Linux distribution for the base image. - Must be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'. + Must be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'. + If omitted, defaults to 'debian' ('latest'). """ pip_config_file: Optional[str] @@ -587,13 +598,28 @@ def validate_config(config: Config) -> Config: ) image_distro = config.get("image_distro", DEFAULT_IMAGE_DISTRO) + internal_docker_tag = config.get("_INTERNAL_docker_tag") + api_version = config.get("api_version") + if internal_docker_tag: + if api_version: + raise click.UsageError( + "Cannot specify both _INTERNAL_docker_tag and api_version." + ) + if api_version: + try: + parts = tuple(map(int, api_version.split("-")[0].split("."))) + if len(parts) > 3: + raise ValueError( + "Version must be major or major.minor or major.minor.patch." + ) + except TypeError: + raise click.UsageError(f"Invalid version format: {api_version}") from None config = { "node_version": node_version, "python_version": python_version, "pip_config_file": config.get("pip_config_file"), "pip_installer": config.get("pip_installer", "auto"), - "_INTERNAL_docker_tag": config.get("_INTERNAL_docker_tag"), "base_image": config.get("base_image"), "image_distro": image_distro, "dependencies": config.get("dependencies", []), @@ -608,6 +634,10 @@ def validate_config(config: Config) -> Config: "ui_config": config.get("ui_config"), "keep_pkg_tools": config.get("keep_pkg_tools"), } + if internal_docker_tag: + config["_INTERNAL_docker_tag"] = internal_docker_tag + if api_version: + config["api_version"] = api_version if config.get("node_version"): node_version = config["node_version"] @@ -644,17 +674,17 @@ def validate_config(config: Config) -> Config: "Add at least one dependency to 'dependencies' list." ) - if not config["graphs"]: + if not config.get("graphs"): raise click.UsageError( "No graphs found in config. Add at least one graph to 'graphs' dictionary." ) # Validate image_distro config if image_distro := config.get("image_distro"): - if image_distro not in ["debian", "wolfi"]: + if image_distro not in Distros.__args__: raise click.UsageError( f"Invalid image_distro: '{image_distro}'. " - "Must be either 'debian' or 'wolfi'." + "Must be one of 'debian', 'bullseye', or 'bookworm'." ) if pip_installer := config.get("pip_installer"): @@ -1465,6 +1495,7 @@ def docker_tag( base_image: Optional[str] = None, api_version: Optional[str] = None, ) -> str: + api_version = api_version or config.get("api_version") base_image = base_image or default_base_image(config) image_distro = config.get("image_distro") @@ -1473,9 +1504,6 @@ def docker_tag( if config.get("_INTERNAL_docker_tag"): return f"{base_image}:{config['_INTERNAL_docker_tag']}" - if "/langgraph-server" in base_image: - return f"{base_image}-py{config['python_version']}" - # Build the standard tag format language, version = None, None if config.get("node_version") and not config.get("python_version"): @@ -1488,6 +1516,8 @@ def docker_tag( # Prepend API version if provided if api_version: full_tag = f"{api_version}-{language}{version_distro_tag}" + elif "/langgraph-server" in base_image and version_distro_tag not in base_image: + return f"{base_image}-{language}{version_distro_tag}" else: full_tag = version_distro_tag diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 4e3523ea8..2fcf7ba72 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph-cli" -version = "0.3.6" +dynamic = ["version"] description = "CLI for interacting with LangGraph API" authors = [] requires-python = ">=3.9" @@ -15,11 +15,12 @@ dependencies = [ "click>=8.1.7", "langgraph-sdk>=0.1.0 ; python_version >= '3.11'", ] - +[tool.hatch.version] +path = "langgraph_cli/__init__.py" [project.optional-dependencies] inmem = [ - "langgraph-api>=0.2.67,<0.3.0 ; python_version >= '3.11'", - "langgraph-runtime-inmem>=0.6.0 ; python_version >= '3.11'", + "langgraph-api>=0.2.120,<0.3.0 ; python_version >= '3.11'", + "langgraph-runtime-inmem>=0.6.8 ; python_version >= '3.11'", "python-dotenv>=0.8.0", ] diff --git a/libs/cli/schemas/schema.json b/libs/cli/schemas/schema.json index e76e69826..b3de933a0 100644 --- a/libs/cli/schemas/schema.json +++ b/libs/cli/schemas/schema.json @@ -15,7 +15,8 @@ "description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n", "enum": [ "3.11", - "3.12" + "3.12", + "3.13" ] }, "pip_config_file": { @@ -40,6 +41,17 @@ ], "description": "Optional. Internal use only.\n" }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n" + }, "auth": { "anyOf": [ { @@ -122,8 +134,9 @@ "image_distro": { "anyOf": [ { - "type": "string", "enum": [ + "bookworm", + "bullseye", "debian", "wolfi" ] @@ -132,7 +145,7 @@ "type": "null" } ], - "description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n" + "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n" }, "keep_pkg_tools": { "anyOf": [ @@ -221,6 +234,17 @@ ], "description": "Optional. Internal use only.\n" }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n" + }, "auth": { "anyOf": [ { @@ -313,7 +337,7 @@ "type": "null" } ], - "description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n" + "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n" }, "keep_pkg_tools": { "anyOf": [ diff --git a/libs/cli/schemas/schema.v0.json b/libs/cli/schemas/schema.v0.json index e76e69826..b3de933a0 100644 --- a/libs/cli/schemas/schema.v0.json +++ b/libs/cli/schemas/schema.v0.json @@ -15,7 +15,8 @@ "description": "Optional. Python version in 'major.minor' format (e.g. '3.11').\nMust be at least 3.11 or greater for this deployment to function properly.\n", "enum": [ "3.11", - "3.12" + "3.12", + "3.13" ] }, "pip_config_file": { @@ -40,6 +41,17 @@ ], "description": "Optional. Internal use only.\n" }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n" + }, "auth": { "anyOf": [ { @@ -122,8 +134,9 @@ "image_distro": { "anyOf": [ { - "type": "string", "enum": [ + "bookworm", + "bullseye", "debian", "wolfi" ] @@ -132,7 +145,7 @@ "type": "null" } ], - "description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n" + "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n" }, "keep_pkg_tools": { "anyOf": [ @@ -221,6 +234,17 @@ ], "description": "Optional. Internal use only.\n" }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional. Which semantic version of the LangGraph API server to use.\n\nDefaults to latest. Check the\nfor more information.\n" + }, "auth": { "anyOf": [ { @@ -313,7 +337,7 @@ "type": "null" } ], - "description": "Optional. Linux distribution for the base image.\n\nMust be either 'debian' or 'wolfi'. If omitted, defaults to 'debian'.\n" + "description": "Optional. Linux distribution for the base image.\n\nMust be one of 'wolfi', 'debian', 'bullseye', or 'bookworm'.\nIf omitted, defaults to 'debian' ('latest').\n" }, "keep_pkg_tools": { "anyOf": [ diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index 413a1f4bc..f0341dbad 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -381,7 +381,9 @@ def test_dockerfile_command_with_base_image() -> None: assert save_path.exists() with open(save_path) as f: dockerfile = f.read() - assert re.match("FROM langchain/langgraph-server:0.2-py3.*", dockerfile) + assert re.match("FROM langchain/langgraph-server:0.2-py3.*", dockerfile), ( + "\n".join(dockerfile.splitlines()[:3]) + ) def test_dockerfile_command_with_docker_compose() -> None: diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 3f1ea7c06..fbc257e8b 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -38,7 +38,6 @@ def test_validate_config(): } actual_config = validate_config(expected_config) expected_config = { - "_INTERNAL_docker_tag": None, "base_image": None, "python_version": "3.11", "node_version": None, @@ -61,7 +60,6 @@ def test_validate_config(): # full config env = ".env" expected_config = { - "_INTERNAL_docker_tag": None, "base_image": None, "python_version": "3.12", "node_version": None, @@ -190,7 +188,6 @@ def test_validate_config_image_distro(): } ) assert "Invalid image_distro: 'ubuntu'" in str(exc_info.value) - assert "Must be either 'debian' or 'wolfi'" in str(exc_info.value) with pytest.raises(click.UsageError) as exc_info: validate_config( @@ -1339,19 +1336,22 @@ def test_docker_tag_different_node_versions_with_distro(): assert tag == expected_tag, f"Failed for Node.js {node_version}" -def test_docker_tag_with_api_version(): +@pytest.mark.parametrize("in_config", [False, True]) +def test_docker_tag_with_api_version(in_config: bool): """Test docker_tag function with api_version parameter.""" # Test 1: Python config with api_version and default distro + version = "0.2.74" config = validate_config( { "python_version": "3.11", "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == "langchain/langgraph-api:0.2.74-py3.11" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraph-api:{version}-py3.11" # Test 2: Python config with api_version and wolfi distro config = validate_config( @@ -1360,20 +1360,22 @@ def test_docker_tag_with_api_version(): "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, "image_distro": "wolfi", + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == "langchain/langgraph-api:0.2.74-py3.12-wolfi" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraph-api:{version}-py3.12-wolfi" # Test 3: Node.js config with api_version and default distro config = validate_config( { "node_version": "20", "graphs": {"agent": "./agent.js:graph"}, + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == "langchain/langgraphjs-api:0.2.74-node20" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraphjs-api:{version}-node20" # Test 4: Node.js config with api_version and wolfi distro config = validate_config( @@ -1381,10 +1383,11 @@ def test_docker_tag_with_api_version(): "node_version": "20", "graphs": {"agent": "./agent.js:graph"}, "image_distro": "wolfi", + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == "langchain/langgraphjs-api:0.2.74-node20-wolfi" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraphjs-api:{version}-node20-wolfi" # Test 5: Custom base image with api_version config = validate_config( @@ -1393,10 +1396,15 @@ def test_docker_tag_with_api_version(): "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, "base_image": "my-registry/custom-image", + "api_version": version if in_config else None, } ) - tag = docker_tag(config, base_image="my-registry/custom-image", api_version="1.0.0") - assert tag == "my-registry/custom-image:1.0.0-py3.11" + tag = docker_tag( + config, + base_image="my-registry/custom-image", + api_version=version if not in_config else None, + ) + assert tag == f"my-registry/custom-image:{version}-py3.11" # Test 6: api_version with different Python versions for python_version in ["3.11", "3.12", "3.13"]: @@ -1405,10 +1413,11 @@ def test_docker_tag_with_api_version(): "python_version": python_version, "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == f"langchain/langgraph-api:0.2.74-py{python_version}" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraph-api:{version}-py{python_version}" # Test 7: Without api_version should work as before config = validate_config( @@ -1428,10 +1437,11 @@ def test_docker_tag_with_api_version(): "node_version": "20", "dependencies": ["."], "graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"}, + "api_version": version if in_config else None, } ) - tag = docker_tag(config, api_version="0.2.74") - assert tag == "langchain/langgraph-api:0.2.74-py3.11" + tag = docker_tag(config, api_version=version if not in_config else None) + assert tag == f"langchain/langgraph-api:{version}-py3.11" # Test 9: api_version with _INTERNAL_docker_tag should ignore api_version config = validate_config( @@ -1451,12 +1461,15 @@ def test_docker_tag_with_api_version(): "python_version": "3.11", "dependencies": ["."], "graphs": {"agent": "./agent.py:graph"}, + "api_version": version if in_config else None, } ) tag = docker_tag( - config, base_image="langchain/langgraph-server:0.2", api_version="0.2.74" + config, + base_image="langchain/langgraph-server", + api_version=version if not in_config else None, ) - assert tag == "langchain/langgraph-server:0.2-py3.11" + assert tag == f"langchain/langgraph-server:{version}-py3.11" def test_config_to_docker_with_api_version(): diff --git a/libs/cli/uv.lock b/libs/cli/uv.lock index 62178a6f3..7b7bae16e 100644 --- a/libs/cli/uv.lock +++ b/libs/cli/uv.lock @@ -471,7 +471,7 @@ wheels = [ [[package]] name = "langgraph" -version = "0.5.3" +version = "0.6.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", marker = "python_full_version >= '3.11'" }, @@ -481,14 +481,14 @@ dependencies = [ { name = "pydantic", marker = "python_full_version >= '3.11'" }, { name = "xxhash", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/f4/f4ebb83dff589b31d4a11c0d3c9c39a55d41f2a722dfb78761f7ed95e96d/langgraph-0.5.3.tar.gz", hash = "sha256:36d4b67f984ff2649d447826fc99b1a2af3e97599a590058f20750048e4f548f", size = 442591, upload-time = "2025-07-14T20:10:02.907Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/2b/59f0b2985467ec84b006dd41ec31c0aae43a7f16722d5514292500b871c9/langgraph-0.6.6.tar.gz", hash = "sha256:e7d3cefacf356f8c01721b166b67b3bf581659d5361a3530f59ecd9b8448eca7", size = 465452, upload-time = "2025-08-20T04:02:13.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/2f/11be9302d3a213debcfe44355453a1e8fd7ee5e3138edeb8bd82b56bc8f6/langgraph-0.5.3-py3-none-any.whl", hash = "sha256:9819b88a6ef6134a0fa6d6121a81b202dc3d17b25cf7ea3fe4d7669b9b252b5d", size = 143774, upload-time = "2025-07-14T20:10:01.497Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ef/81fce0a80925cd89987aa641ff01573e3556a24f2d205112862a69df7fd3/langgraph-0.6.6-py3-none-any.whl", hash = "sha256:a2283a5236abba6c8307c1a485c04e8a0f0ffa2be770878782a7bf2deb8d7954", size = 153274, upload-time = "2025-08-20T04:02:12.251Z" }, ] [[package]] name = "langgraph-api" -version = "0.2.96" +version = "0.2.137" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle", marker = "python_full_version >= '3.11'" }, @@ -511,9 +511,9 @@ dependencies = [ { name = "uvicorn", marker = "python_full_version >= '3.11'" }, { name = "watchfiles", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/4c/837c5ce4aab704b6b13f27c5dd6330dabaf2f25d198032cb18e5d5dcaa53/langgraph_api-0.2.96.tar.gz", hash = "sha256:c498b5542a952d194121cdbe5a4b04e2f48fbc37480141ea2b87ba39a132ddb1", size = 238776, upload-time = "2025-07-17T17:57:47.274Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/13/bb3601d76d35f285564ff4b963f3c946aaa854ebc910d89d5ed535a8e2e8/langgraph_api-0.2.137.tar.gz", hash = "sha256:7791bda6ae91e305b3bc95ece338c67dd89f3d5cf735e7ef1ffe381fc2629b44", size = 255804, upload-time = "2025-08-20T07:32:43.969Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/7f/dfae9bc0f85a98bbd96d00df2a39e8b8386977e8ce4a6199d1065bb3709d/langgraph_api-0.2.96-py3-none-any.whl", hash = "sha256:304d424d7a85735489fab1764b439e8219739619ad708b9465b8b8f421f17b37", size = 194393, upload-time = "2025-07-17T17:57:45.89Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d3/bbf69e3443313a014cbe1365f21f7930dd86e2273b6d1aa1d75f03db5221/langgraph_api-0.2.137-py3-none-any.whl", hash = "sha256:42a914903a2722fc12e846f4607a2b5d9f39a084fe091036f3385d9b69c9c8e8", size = 206034, upload-time = "2025-08-20T07:32:42.498Z" }, ] [[package]] @@ -531,7 +531,6 @@ wheels = [ [[package]] name = "langgraph-cli" -version = "0.3.6" source = { editable = "." } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -561,8 +560,8 @@ dev = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.7" }, - { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67,<0.3.0" }, - { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.6.0" }, + { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.120,<0.3.0" }, + { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.6.8" }, { name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" }, { name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" }, ] @@ -582,20 +581,20 @@ dev = [ [[package]] name = "langgraph-prebuilt" -version = "0.5.2" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core", marker = "python_full_version >= '3.11'" }, { name = "langgraph-checkpoint", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/11/98134c47832fbde0caf0e06f1a104577da9215c358d7854093c1d835b272/langgraph_prebuilt-0.5.2.tar.gz", hash = "sha256:2c900a5be0d6a93ea2521e0d931697cad2b646f1fcda7aa5c39d8d7539772465", size = 117808, upload-time = "2025-06-30T19:52:48.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/21/9b198d11732101ee8cdf30af98d0b4f11254c768de15173e57f5260fd14b/langgraph_prebuilt-0.6.4.tar.gz", hash = "sha256:e9e53b906ee5df46541d1dc5303239e815d3ec551e52bb03dd6463acc79ec28f", size = 125695, upload-time = "2025-08-07T18:17:57.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/64/6bc45ab9e0e1112698ebff579fe21f5606ea65cd08266995a357e312a4d2/langgraph_prebuilt-0.5.2-py3-none-any.whl", hash = "sha256:1f4cd55deca49dffc3e5127eec12fcd244fc381321002f728afa88642d5ec59d", size = 23776, upload-time = "2025-06-30T19:52:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7f/973b0d9729d9693d6e5b4bc5f3ae41138d194cb7b16b0ed230020beeb13a/langgraph_prebuilt-0.6.4-py3-none-any.whl", hash = "sha256:819f31d88b84cb2729ff1b79db2d51e9506b8fb7aaacfc0d359d4fe16e717344", size = 28025, upload-time = "2025-08-07T18:17:56.493Z" }, ] [[package]] name = "langgraph-runtime-inmem" -version = "0.6.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blockbuster", marker = "python_full_version >= '3.11'" }, @@ -605,22 +604,22 @@ dependencies = [ { name = "starlette", marker = "python_full_version >= '3.11'" }, { name = "structlog", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0c/d145c6d83d36efda17b10812760711b77ec05f5bbe962c961d75b32e3c17/langgraph_runtime_inmem-0.6.0.tar.gz", hash = "sha256:b09675789a331be4a2b387c9c46de8772c4c8418e74c057b4ca24e85c25acae3", size = 77618, upload-time = "2025-07-17T16:51:01.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/94/02b58b1c137cfca6507f6c495e5794fd542b1f5269ef89fb662799be4b8c/langgraph_runtime_inmem-0.8.0.tar.gz", hash = "sha256:3082273f65650665b4a3875241721087fd51e675eb0227b18dc271839ce99594", size = 79510, upload-time = "2025-08-18T09:00:09.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/6a/9dc5769b5d2f97d1feacbbf93b180c359dff7462454b37dfef8aed4ebcf7/langgraph_runtime_inmem-0.6.0-py3-none-any.whl", hash = "sha256:312dab25bec6557f1edf95cb8bd7c8bb52f7f4bfeecaf66e7001662f095c9079", size = 29317, upload-time = "2025-07-17T16:51:00.622Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0e/b09c1aff0bcfdb1357be212b4a5d55f5b98644eae7fab6b115aa463e99fa/langgraph_runtime_inmem-0.8.0-py3-none-any.whl", hash = "sha256:85398321fc186618b0957c4d8629cc059fce2e7f57a4756ef83a75575791da1b", size = 31626, upload-time = "2025-08-18T09:00:08.256Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.1.73" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx", marker = "python_full_version >= '3.11'" }, { name = "orjson", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/e8/daf0271f91e93b10566533955c00ee16e471066755c2efd1ba9a887a7eab/langgraph_sdk-0.1.73.tar.gz", hash = "sha256:6e6dcdf66bcf8710739899616856527a72a605ce15beb76fbac7f4ce0e2ad080", size = 72157, upload-time = "2025-07-14T23:57:22.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/3a/ea929b5b3827615802f020abdaa6d4a6f9d59ab764f65559fa6f87a6dda6/langgraph_sdk-0.2.2.tar.gz", hash = "sha256:9484e8071953df75d7aaf9845d82db3595e485af7d5dcc235c9b32c52362e1fc", size = 77981, upload-time = "2025-08-18T19:25:42.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/86/56e01e715e5b0028cdaff1492a89e54fa12e18c21e03b805a10ea36ecd5a/langgraph_sdk-0.1.73-py3-none-any.whl", hash = "sha256:a60ac33f70688ad07051edff1d5ed8089c8f0de1f69dc900be46e095ca20eed8", size = 50222, upload-time = "2025-07-14T23:57:21.42Z" }, + { url = "https://files.pythonhosted.org/packages/01/0d/dfa633c6b85e973e7d4383e9b92603b7e910e89768411daeb7777bfbae04/langgraph_sdk-0.2.2-py3-none-any.whl", hash = "sha256:1afbec01ade166f8b6ce18782875415422eb70dcb82852aeaa373e6152db4b82", size = 52017, upload-time = "2025-08-18T19:25:40.567Z" }, ] [[package]] diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 5ad1efc32..d49ec5a55 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1474,7 +1474,6 @@ dev = [ [[package]] name = "langgraph-cli" -version = "0.3.6" source = { editable = "../cli" } dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -1492,8 +1491,8 @@ inmem = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.7" }, - { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.67,<0.3.0" }, - { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.6.0" }, + { name = "langgraph-api", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.2.120,<0.3.0" }, + { name = "langgraph-runtime-inmem", marker = "python_full_version >= '3.11' and extra == 'inmem'", specifier = ">=0.6.8" }, { name = "langgraph-sdk", marker = "python_full_version >= '3.11'", specifier = ">=0.1.0" }, { name = "python-dotenv", marker = "extra == 'inmem'", specifier = ">=0.8.0" }, ]