diff --git a/docs/docs/cloud/reference/langgraph_server_changelog.md b/docs/docs/cloud/reference/langgraph_server_changelog.md index 66e1702af..8a6823e3f 100644 --- a/docs/docs/cloud/reference/langgraph_server_changelog.md +++ b/docs/docs/cloud/reference/langgraph_server_changelog.md @@ -4,6 +4,28 @@ --- +## v0.2.102 (2025-07-24) +- Captured interrupt events in the wait method to preserve legacy behavior and stream updates by default. +- Added support for SDK structlog in the JavaScript environment, enhancing logging capabilities. + +## v0.2.101 (2025-07-24) +- Used the correct metadata endpoint for self-hosted environments, resolving an access issue. + +## v0.2.99 (2025-07-22) +- Improved license validation by adding an in-memory cache and handling Redis connection errors more effectively. +- Automatically remove agents from memory that are removed from `langgraph.json` to prevent persistence issues. +- Ensured the UI namespace for generated UI is a valid JavaScript property name to prevent errors. +- Raised a 422 error for improved request validation feedback. + +## v0.2.98 (2025-07-19) +- Added langgraph node context for improved log filtering and trace visibility. + +## v0.2.97 (2025-07-19) +- Fixed scheduling issue with ckpt ingestion worker that occurred on isolated background loops. +- Ensured queue worker starts only after all migrations have completed. +- Added more detailed error messages for thread state issues and improved response handling when state updates fail. +- Exposed interrupt ID while retrieving thread state for enhanced API response details. + ## v0.2.96 (2025-07-17) - Added a fallback mechanism for configurable header patterns to handle exclude/include settings more effectively. diff --git a/docs/docs/concepts/langgraph_control_plane.md b/docs/docs/concepts/langgraph_control_plane.md index 006a57df9..e689a1a79 100644 --- a/docs/docs/concepts/langgraph_control_plane.md +++ b/docs/docs/concepts/langgraph_control_plane.md @@ -119,6 +119,11 @@ These metrics are displayed as charts in the Control Plane UI. ### LangSmith Integration -A [LangSmith](https://docs.smith.langchain.com/) tracing project is automatically created for each deployment. The tracing project has the same name as the deployment. When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane. +A [LangSmith](https://docs.smith.langchain.com/) tracing project and LangSmith API key are automatically created for each deployment. The deployment uses the API key to automatically send traces to LangSmith. -When a deployment is deleted, the traces and the tracing project are not deleted. +- The tracing project has the same name as the deployment. +- The API key has the description `LangGraph Platform: `. +- The API key is never revealed and cannot be deleted manually. +- When creating a deployment, the `LANGCHAIN_TRACING` and `LANGSMITH_API_KEY`/`LANGCHAIN_API_KEY` environment variables do not need to be specified; they are set automatically by the control plane. + +When a deployment is deleted, the traces and the tracing project are not deleted. However, the API will be deleted when the deployment is deleted. diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 91980d3ae..0ee774159 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -153,6 +153,12 @@ OPT_POSTGRES_URI = click.option( help="Postgres URI to use for the database. Defaults to launching a local database", ) +OPT_API_VERSION = click.option( + "--api-version", + type=str, + help="API server version to use for the base image. If unspecified, the latest version will be used.", +) + @click.group() @click.version_option(version=__version__, prog_name="LangGraph CLI") @@ -170,6 +176,7 @@ def cli(): @OPT_DEBUGGER_BASE_URL @OPT_WATCH @OPT_POSTGRES_URI +@OPT_API_VERSION @click.option( "--image", type=str, @@ -203,6 +210,7 @@ def up( debugger_port: Optional[int], debugger_base_url: Optional[str], postgres_uri: Optional[str], + api_version: Optional[str], image: Optional[str], base_image: Optional[str], ): @@ -225,6 +233,7 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE debugger_port=debugger_port, debugger_base_url=debugger_base_url, postgres_uri=postgres_uri, + api_version=api_version, image=image, base_image=base_image, ) @@ -290,6 +299,7 @@ def _build( config: pathlib.Path, config_json: dict, base_image: Optional[str], + api_version: Optional[str], pull: bool, tag: str, passthrough: Sequence[str] = (), @@ -300,7 +310,7 @@ def _build( subp_exec( "docker", "pull", - langgraph_cli.config.docker_tag(config_json, base_image), + langgraph_cli.config.docker_tag(config_json, base_image, api_version), verbose=True, ) ) @@ -314,7 +324,7 @@ def _build( ] # apply config stdin, additional_contexts = langgraph_cli.config.config_to_docker( - config, config_json, base_image + config, config_json, base_image, api_version ) # add additional_contexts if additional_contexts: @@ -355,6 +365,7 @@ def _build( "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version" "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)", ) +@OPT_API_VERSION @click.argument("docker_build_args", nargs=-1, type=click.UNPROCESSED) @cli.command( help="📦 Build LangGraph API server Docker image.", @@ -367,6 +378,7 @@ def build( config: pathlib.Path, docker_build_args: Sequence[str], base_image: Optional[str], + api_version: Optional[str], pull: bool, tag: str, ): @@ -376,7 +388,15 @@ def build( config_json = langgraph_cli.config.validate_config_file(config) warn_non_wolfi_distro(config_json) _build( - runner, set, config, config_json, base_image, pull, tag, docker_build_args + runner, + set, + config, + config_json, + base_image, + api_version, + pull, + tag, + docker_build_args, ) @@ -456,12 +476,14 @@ tests "\n\n \b\nExamples:\n --base-image langchain/langgraph-server:0.2.18 # Pin to a specific patch version" "\n --base-image langchain/langgraph-server:0.2 # Pin to a minor version (Python)", ) +@OPT_API_VERSION @log_command def dockerfile( save_path: str, config: pathlib.Path, add_docker_compose: bool, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> None: save_path = pathlib.Path(save_path).absolute() secho(f"🔍 Validating configuration at path: {config}", fg="yellow") @@ -474,6 +496,7 @@ def dockerfile( config, config_json, base_image=base_image, + api_version=api_version, ) with open(str(save_path), "w", encoding="utf-8") as f: f.write(dockerfile) @@ -739,6 +762,7 @@ def prepare_args_and_stdin( debugger_port: Optional[int] = None, debugger_base_url: Optional[str] = None, postgres_uri: Optional[str] = None, + api_version: Optional[str] = None, # Like "my-tag" (if you already built it locally) image: Optional[str] = None, # Like "langchain/langgraphjs-api" or "langchain/langgraph-api @@ -754,6 +778,7 @@ def prepare_args_and_stdin( postgres_uri=postgres_uri, image=image, # Pass image to compose YAML generator base_image=base_image, + api_version=api_version, ) args = [ "--project-directory", @@ -769,6 +794,7 @@ def prepare_args_and_stdin( config, watch=watch, base_image=langgraph_cli.config.default_base_image(config), + api_version=api_version, image=image, ) return args, stdin @@ -787,6 +813,7 @@ def prepare( debugger_port: Optional[int] = None, debugger_base_url: Optional[str] = None, postgres_uri: Optional[str] = None, + api_version: Optional[str] = None, image: Optional[str] = None, base_image: Optional[str] = None, ) -> tuple[list[str], str]: @@ -799,7 +826,7 @@ def prepare( subp_exec( "docker", "pull", - langgraph_cli.config.docker_tag(config_json, base_image), + langgraph_cli.config.docker_tag(config_json, base_image, api_version), verbose=verbose, ) ) @@ -814,6 +841,7 @@ def prepare( debugger_port=debugger_port, debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}", postgres_uri=postgres_uri, + api_version=api_version, image=image, base_image=base_image, ) diff --git a/libs/cli/langgraph_cli/config.py b/libs/cli/langgraph_cli/config.py index 9030c3ba2..43a940afc 100644 --- a/libs/cli/langgraph_cli/config.py +++ b/libs/cli/langgraph_cli/config.py @@ -1213,6 +1213,7 @@ def python_config_to_docker( config_path: pathlib.Path, config: Config, base_image: str, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: """Generate a Dockerfile from the configuration.""" pip_installer = config.get("pip_installer", "auto") @@ -1360,7 +1361,7 @@ ADD {relpath} /deps/{name} "# -- End of JS dependencies install --", ] ) - image_str = docker_tag(config, base_image) + image_str = docker_tag(config, base_image, api_version) docker_file_contents = [ f"FROM {image_str}", "", @@ -1402,10 +1403,11 @@ def node_config_to_docker( config_path: pathlib.Path, config: Config, base_image: str, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: faux_path = f"/deps/{config_path.parent.name}" install_cmd = _get_node_pm_install_cmd(config_path, config) - image_str = docker_tag(config, base_image) + image_str = docker_tag(config, base_image, api_version) env_vars: list[str] = [] @@ -1461,6 +1463,7 @@ def default_base_image(config: Config) -> str: def docker_tag( config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> str: base_image = base_image or default_base_image(config) @@ -1473,28 +1476,43 @@ def 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"): - return f"{base_image}:{config['node_version']}{distro_tag}" - return f"{base_image}:{config['python_version']}{distro_tag}" + language, version = "node", config["node_version"] + else: + language, version = "py", config["python_version"] + + version_distro_tag = f"{version}{distro_tag}" + + # Prepend API version if provided + if api_version: + full_tag = f"{api_version}-{language}{version_distro_tag}" + else: + full_tag = version_distro_tag + + return f"{base_image}:{full_tag}" def config_to_docker( config_path: pathlib.Path, config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> tuple[str, dict[str, str]]: base_image = base_image or default_base_image(config) if config.get("node_version") and not config.get("python_version"): - return node_config_to_docker(config_path, config, base_image) + return node_config_to_docker(config_path, config, base_image, api_version) - return python_config_to_docker(config_path, config, base_image) + return python_config_to_docker(config_path, config, base_image, api_version) def config_to_compose( config_path: pathlib.Path, config: Config, base_image: Optional[str] = None, + api_version: Optional[str] = None, image: Optional[str] = None, watch: bool = False, ) -> str: @@ -1531,7 +1549,7 @@ def config_to_compose( else: dockerfile, additional_contexts = config_to_docker( - config_path, config, base_image + config_path, config, base_image, api_version ) additional_contexts_str = "\n".join( diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index ff820b1da..614dc6369 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -147,6 +147,8 @@ def compose_as_dict( image: Optional[str] = None, # Base image to use for the LangGraph API server base_image: Optional[str] = None, + # API version of the base image + api_version: Optional[str] = None, ) -> dict: """Create a docker compose file as a dictionary in YML style.""" if postgres_uri is None: @@ -252,6 +254,7 @@ def compose( postgres_uri: Optional[str] = None, image: Optional[str] = None, base_image: Optional[str] = None, + api_version: Optional[str] = None, ) -> str: """Create a docker compose file as a string.""" compose_content = compose_as_dict( @@ -262,6 +265,7 @@ def compose( postgres_uri=postgres_uri, image=image, base_image=base_image, + api_version=api_version, ) compose_str = dict_to_yaml(compose_content) return compose_str diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 757e92b55..4e3523ea8 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.5" +version = "0.3.6" description = "CLI for interacting with LangGraph API" authors = [] requires-python = ">=3.9" diff --git a/libs/cli/tests/unit_tests/cli/test_cli.py b/libs/cli/tests/unit_tests/cli/test_cli.py index e6d0a17b4..413a1f4bc 100644 --- a/libs/cli/tests/unit_tests/cli/test_cli.py +++ b/libs/cli/tests/unit_tests/cli/test_cli.py @@ -574,3 +574,248 @@ def test_build_generate_proper_build_context(): assert len(build_contexts) == 2, ( f"Expected 2 build contexts, but found {len(build_contexts)}" ) + + +def test_dockerfile_command_with_api_version() -> None: + """Test the 'dockerfile' command with --api-version flag.""" + runner = CliRunner() + config_content = { + "python_version": "3.11", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.py" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", + str(temp_dir / "config.json"), + "--api-version", + "0.2.74", + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM langchain/langgraph-api:0.2.74-py3.11" in dockerfile + + +def test_dockerfile_command_with_api_version_and_base_image() -> None: + """Test the 'dockerfile' command with both --api-version and --base-image flags.""" + runner = CliRunner() + config_content = { + "python_version": "3.12", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.py" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", + str(temp_dir / "config.json"), + "--api-version", + "1.0.0", + "--base-image", + "my-registry/custom-api", + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM my-registry/custom-api:1.0.0-py3.12-wolfi" in dockerfile + + +def test_dockerfile_command_with_api_version_nodejs() -> None: + """Test the 'dockerfile' command with --api-version flag for Node.js config.""" + runner = CliRunner() + config_content = { + "node_version": "20", + "graphs": {"agent": "agent.js:graph"}, + } + + with temporary_config_folder(config_content) as temp_dir: + save_path = temp_dir / "Dockerfile" + agent_path = temp_dir / "agent.js" + agent_path.touch() + + result = runner.invoke( + cli, + [ + "dockerfile", + str(save_path), + "--config", + str(temp_dir / "config.json"), + "--api-version", + "0.2.74", + ], + ) + + # Assert command was successful + assert result.exit_code == 0, result.output + assert "✅ Created: Dockerfile" in result.output + + # Check if Dockerfile was created and contains correct FROM line + assert save_path.exists() + with open(save_path) as f: + dockerfile = f.read() + assert "FROM langchain/langgraphjs-api:0.2.74-node20" in dockerfile + + +def test_build_command_with_api_version() -> None: + """Test the 'build' command with --api-version flag.""" + runner = CliRunner() + config_content = { + "python_version": "3.11", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", # Use wolfi to avoid warning messages + } + + with temporary_config_folder(config_content) as temp_dir: + agent_path = temp_dir / "agent.py" + agent_path.touch() + + # Mock docker command since we don't want to actually build + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + [ + "build", + "--tag", + "test-image", + "--config", + str(temp_dir / "config.json"), + "--api-version", + "0.2.74", + "--no-pull", # Avoid pulling non-existent images + ], + catch_exceptions=True, + ) + + # Check that the build command is called with the correct tag + # The output should contain the docker build command with the api_version tag + assert "langchain/langgraph-api:0.2.74-py3.11-wolfi" in result.output + + +def test_build_command_with_api_version_and_base_image() -> None: + """Test the 'build' command with both --api-version and --base-image flags.""" + runner = CliRunner() + config_content = { + "python_version": "3.12", + "graphs": {"agent": "agent.py:graph"}, + "dependencies": ["."], + "image_distro": "wolfi", # Use wolfi to avoid warning messages + } + + with temporary_config_folder(config_content) as temp_dir: + agent_path = temp_dir / "agent.py" + agent_path.touch() + + # Mock docker command since we don't want to actually build + with runner.isolated_filesystem(): + result = runner.invoke( + cli, + [ + "build", + "--tag", + "test-image", + "--config", + str(temp_dir / "config.json"), + "--api-version", + "1.0.0", + "--base-image", + "my-registry/custom-api", + "--no-pull", # Avoid pulling non-existent images + ], + catch_exceptions=True, + ) + + # Check that the build command includes the api_version + assert "my-registry/custom-api:1.0.0-py3.12-wolfi" in result.output + + +def test_prepare_args_and_stdin_with_api_version() -> None: + """Test prepare_args_and_stdin function with api_version parameter.""" + config_path = pathlib.Path(__file__).parent / "langgraph.json" + config = validate_config( + Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + ) + port = 8000 + api_version = "0.2.74" + + actual_args, actual_stdin = prepare_args_and_stdin( + capabilities=DEFAULT_DOCKER_CAPABILITIES, + config_path=config_path, + config=config, + docker_compose=None, + port=port, + watch=False, + api_version=api_version, + ) + + expected_args = [ + "--project-directory", + str(pathlib.Path(__file__).parent.absolute()), + "-f", + "-", + ] + + # Check that the args are correct + assert actual_args == expected_args + + # Check that the stdin contains the correct FROM line with api_version + assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_stdin + + +def test_prepare_args_and_stdin_with_api_version_and_image() -> None: + """Test prepare_args_and_stdin function with both api_version and image parameters.""" + config_path = pathlib.Path(__file__).parent / "langgraph.json" + config = validate_config( + Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) + ) + port = 8000 + api_version = "0.2.74" + image = "my-custom-image:latest" + + actual_args, actual_stdin = prepare_args_and_stdin( + capabilities=DEFAULT_DOCKER_CAPABILITIES, + config_path=config_path, + config=config, + docker_compose=None, + port=port, + watch=False, + api_version=api_version, + image=image, + ) + + # When image is provided, api_version should be ignored for the image + # but the stdin should not contain a build section (since image is provided) + assert "pull_policy: build" not in actual_stdin diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 691c1e888..3f1ea7c06 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -1337,3 +1337,195 @@ def test_docker_tag_different_node_versions_with_distro(): ) tag = docker_tag(config) assert tag == expected_tag, f"Failed for Node.js {node_version}" + + +def test_docker_tag_with_api_version(): + """Test docker_tag function with api_version parameter.""" + + # Test 1: Python config with api_version and default distro + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-py3.11" + + # Test 2: Python config with api_version and wolfi distro + config = validate_config( + { + "python_version": "3.12", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "image_distro": "wolfi", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-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"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraphjs-api:0.2.74-node20" + + # Test 4: Node.js config with api_version and wolfi distro + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + "image_distro": "wolfi", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraphjs-api:0.2.74-node20-wolfi" + + # Test 5: Custom base image with api_version + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "base_image": "my-registry/custom-image", + } + ) + 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" + + # Test 6: api_version with different Python versions + for python_version in ["3.11", "3.12", "3.13"]: + config = validate_config( + { + "python_version": python_version, + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == f"langchain/langgraph-api:0.2.74-py{python_version}" + + # Test 7: Without api_version should work as before + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag(config) + assert tag == "langchain/langgraph-api:3.11" + + # Test 8: api_version with multiplatform config (should default to Python) + config = validate_config( + { + "python_version": "3.11", + "node_version": "20", + "dependencies": ["."], + "graphs": {"python": "./agent.py:graph", "js": "./agent.js:graph"}, + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:0.2.74-py3.11" + + # Test 9: api_version with _INTERNAL_docker_tag should ignore api_version + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + "_INTERNAL_docker_tag": "internal-tag", + } + ) + tag = docker_tag(config, api_version="0.2.74") + assert tag == "langchain/langgraph-api:internal-tag" + + # Test 10: api_version with langgraph-server base image should follow special format + config = validate_config( + { + "python_version": "3.11", + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + tag = docker_tag( + config, base_image="langchain/langgraph-server:0.2", api_version="0.2.74" + ) + assert tag == "langchain/langgraph-server:0.2-py3.11" + + +def test_config_to_docker_with_api_version(): + """Test config_to_docker function with api_version parameter.""" + + # Test Python config with api_version + graphs = {"agent": "./agent.py:graph"} + actual_docker_stdin, additional_contexts = config_to_docker( + PATH_TO_CONFIG, + validate_config({"dependencies": ["."], "graphs": graphs}), + "langchain/langgraph-api", + api_version="0.2.74", + ) + + # Check that the FROM line uses the api_version + lines = actual_docker_stdin.split("\n") + from_line = lines[0] + assert from_line == "FROM langchain/langgraph-api:0.2.74-py3.11" + + # Test Node.js config with api_version + graphs = {"agent": "./agent.js:graph"} + actual_docker_stdin, additional_contexts = config_to_docker( + PATH_TO_CONFIG, + validate_config({"node_version": "20", "graphs": graphs}), + "langchain/langgraphjs-api", + api_version="0.2.74", + ) + + # Check that the FROM line uses the api_version + lines = actual_docker_stdin.split("\n") + from_line = lines[0] + assert from_line == "FROM langchain/langgraphjs-api:0.2.74-node20" + + +def test_config_to_compose_with_api_version(): + """Test config_to_compose function with api_version parameter.""" + + # Test Python config with api_version + config = validate_config( + { + "dependencies": ["."], + "graphs": {"agent": "./agent.py:graph"}, + } + ) + + actual_compose_str = config_to_compose( + PATH_TO_CONFIG, + config, + "langchain/langgraph-api", + api_version="0.2.74", + ) + + # Check that the compose file includes the correct FROM line with api_version + assert "FROM langchain/langgraph-api:0.2.74-py3.11" in actual_compose_str + + # Test Node.js config with api_version + config = validate_config( + { + "node_version": "20", + "graphs": {"agent": "./agent.js:graph"}, + } + ) + + actual_compose_str = config_to_compose( + PATH_TO_CONFIG, + config, + "langchain/langgraphjs-api", + api_version="0.2.74", + ) + + # Check that the compose file includes the correct FROM line with api_version + assert "FROM langchain/langgraphjs-api:0.2.74-node20" in actual_compose_str diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py index 355efeb4d..b29118a26 100644 --- a/libs/cli/tests/unit_tests/test_docker.py +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -146,3 +146,220 @@ services: REDIS_URI: redis://langgraph-redis:6379 POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_api_version(): + """Test compose function with api_version parameter.""" + port = 8123 + api_version = "0.2.74" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, port=port, api_version=api_version + ) + + # The compose function should generate a compose file that doesn't directly + # reference the api_version, since it's handled in the docker tag creation + # when building the image. The compose function mainly sets up services. + expected_compose_str = f"""volumes: + langgraph-data: + driver: local +services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 + langgraph-postgres: + image: pgvector/pgvector:pg16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + command: + - postgres + - -c + - shared_preload_libraries=vector + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 + interval: 5s + langgraph-api: + ports: + - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy + langgraph-postgres: + condition: service_healthy + environment: + REDIS_URI: redis://langgraph-redis:6379 + POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_api_version_and_base_image(): + """Test compose function with both api_version and base_image parameters.""" + port = 8123 + api_version = "1.0.0" + base_image = "my-registry/custom-api" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version, + base_image=base_image, + ) + + # Similar to the previous test - the compose function doesn't directly embed + # the api_version or base_image into the compose file since those are handled + # during the docker build process + expected_compose_str = f"""volumes: + langgraph-data: + driver: local +services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 + langgraph-postgres: + image: pgvector/pgvector:pg16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + command: + - postgres + - -c + - shared_preload_libraries=vector + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 + interval: 5s + langgraph-api: + ports: + - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy + langgraph-postgres: + condition: service_healthy + environment: + REDIS_URI: redis://langgraph-redis:6379 + POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_api_version_and_custom_postgres(): + """Test compose function with api_version and custom postgres URI.""" + port = 8123 + api_version = "0.2.74" + custom_postgres_uri = "postgresql://user:pass@external-db:5432/mydb" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version, + postgres_uri=custom_postgres_uri, + ) + + expected_compose_str = f"""services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 + langgraph-api: + ports: + - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy + environment: + REDIS_URI: redis://langgraph-redis:6379 + POSTGRES_URI: {custom_postgres_uri}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str + + +def test_compose_with_api_version_and_debugger(): + """Test compose function with api_version and debugger port.""" + port = 8123 + debugger_port = 8001 + api_version = "0.2.74" + + actual_compose_str = compose( + DEFAULT_DOCKER_CAPABILITIES, + port=port, + api_version=api_version, + debugger_port=debugger_port, + ) + + expected_compose_str = f"""volumes: + langgraph-data: + driver: local +services: + langgraph-redis: + image: redis:6 + healthcheck: + test: redis-cli ping + interval: 5s + timeout: 1s + retries: 5 + langgraph-postgres: + image: pgvector/pgvector:pg16 + ports: + - "5433:5432" + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + command: + - postgres + - -c + - shared_preload_libraries=vector + volumes: + - langgraph-data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + start_period: 10s + timeout: 1s + retries: 5 + interval: 5s + langgraph-debugger: + image: langchain/langgraph-debugger + restart: on-failure + depends_on: + langgraph-postgres: + condition: service_healthy + ports: + - "{debugger_port}:3968" + langgraph-api: + ports: + - "{port}:8000" + depends_on: + langgraph-redis: + condition: service_healthy + langgraph-postgres: + condition: service_healthy + environment: + REDIS_URI: redis://langgraph-redis:6379 + POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" + assert clean_empty_lines(actual_compose_str) == expected_compose_str diff --git a/libs/cli/uv.lock b/libs/cli/uv.lock index c3bc3e0a7..62178a6f3 100644 --- a/libs/cli/uv.lock +++ b/libs/cli/uv.lock @@ -531,7 +531,7 @@ wheels = [ [[package]] name = "langgraph-cli" -version = "0.3.5" +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'" }, diff --git a/libs/langgraph/langgraph/pregel/remote.py b/libs/langgraph/langgraph/pregel/remote.py index 6cc51b908..fbdc1615b 100644 --- a/libs/langgraph/langgraph/pregel/remote.py +++ b/libs/langgraph/langgraph/pregel/remote.py @@ -8,6 +8,7 @@ from typing import ( cast, ) +import langsmith as ls from langchain_core.runnables import RunnableConfig from langchain_core.runnables.graph import ( Edge as DrawableEdge, @@ -118,6 +119,7 @@ class RemoteGraph(PregelProtocol): sync_client: SyncLangGraphClient | None = None, config: RunnableConfig | None = None, name: str | None = None, + distributed_tracing: bool = False, ): """Specify `url`, `api_key`, and/or `headers` to create default sync and async clients. @@ -136,6 +138,7 @@ class RemoteGraph(PregelProtocol): name: Human-readable name to attach to the RemoteGraph instance. This is useful for adding `RemoteGraph` as a subgraph via `graph.add_node(remote_graph)`. If not provided, defaults to the assistant ID. + distributed_tracing: Whether to enable sending LangSmith distributed tracing headers. """ self.assistant_id = assistant_id if name is None: @@ -143,6 +146,7 @@ class RemoteGraph(PregelProtocol): else: self.name = name self.config = config + self.distributed_tracing = distributed_tracing if client is None and url is not None: client = get_client(url=url, api_key=api_key, headers=headers) @@ -672,6 +676,7 @@ class RemoteGraph(PregelProtocol): interrupt_after=interrupt_after, stream_subgraphs=subgraphs or stream is not None, if_not_exists="create", + headers=self._merge_tracing_headers(kwargs.pop("headers", None) or {}), **kwargs, ): # split mode and ns @@ -774,6 +779,7 @@ class RemoteGraph(PregelProtocol): interrupt_after=interrupt_after, stream_subgraphs=subgraphs or stream is not None, if_not_exists="create", + headers=self._merge_tracing_headers(kwargs.pop("headers", None) or {}), **kwargs, ): # split mode and ns @@ -909,3 +915,13 @@ class RemoteGraph(PregelProtocol): return chunk except UnboundLocalError: return None + + def _merge_tracing_headers(self, headers: dict[str, str]) -> dict[str, str]: + if rt := ls.get_current_run_tree(): + tracing_headers = rt.to_headers() + baggage = tracing_headers.pop("baggage") + if "baggage" in headers: + baggage = headers["baggage"] + "," + baggage + tracing_headers["baggage"] = baggage + headers.update(tracing_headers) + return headers diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 065815c66..3257a9fc2 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "langchain-core>=0.1", "langgraph-checkpoint>=2.1.0,<3.0.0", "langgraph-sdk>=0.2.0,<0.3.0", - "langgraph-prebuilt>=0.5.0,<0.6.0", + "langgraph-prebuilt>=0.6.0,<0.7.0", "xxhash>=3.5.0", "pydantic>=2.7.4", ] diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index f979cf4d7..88d960853 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1394,7 +1394,7 @@ dev = [ [[package]] name = "langgraph-cli" -version = "0.3.5" +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'" }, @@ -1433,7 +1433,7 @@ dev = [ [[package]] name = "langgraph-prebuilt" -version = "0.5.2" +version = "0.6.0" source = { editable = "../prebuilt" } dependencies = [ { name = "langchain-core" }, @@ -1481,7 +1481,7 @@ wheels = [ [[package]] name = "langgraph-sdk" -version = "0.2.0a1" +version = "0.2.0" source = { editable = "../sdk-py" } dependencies = [ { name = "httpx" }, diff --git a/libs/prebuilt/pyproject.toml b/libs/prebuilt/pyproject.toml index a4d389343..535a9f2e5 100644 --- a/libs/prebuilt/pyproject.toml +++ b/libs/prebuilt/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph-prebuilt" -version = "0.5.2" +version = "0.6.0" description = "Library with high-level APIs for creating and executing LangGraph agents and tools." authors = [] requires-python = ">=3.9" diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index a300dc1a3..b7ea56788 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -460,7 +460,7 @@ dev = [ [[package]] name = "langgraph-prebuilt" -version = "0.5.2" +version = "0.6.0" source = { editable = "." } dependencies = [ { name = "langchain-core" }, @@ -507,7 +507,7 @@ dev = [ [[package]] name = "langgraph-sdk" -version = "0.2.0a1" +version = "0.2.0" source = { editable = "../sdk-py" } dependencies = [ { name = "httpx" }, diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index df8c94fe7..48f19a2a7 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "langgraph-sdk" -version = "0.2.0a1" +version = "0.2.0" description = "SDK for interacting with LangGraph API" authors = [] requires-python = ">=3.9" diff --git a/libs/sdk-py/uv.lock b/libs/sdk-py/uv.lock index 8e9b727e0..bdf81803f 100644 --- a/libs/sdk-py/uv.lock +++ b/libs/sdk-py/uv.lock @@ -119,7 +119,7 @@ wheels = [ [[package]] name = "langgraph-sdk" -version = "0.2.0a1" +version = "0.2.0" source = { editable = "." } dependencies = [ { name = "httpx" },