From 5697f0716301dd150f3ecb906c245619ee759342 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 26 Jun 2024 11:53:44 -0700 Subject: [PATCH] cli: Reduce to test and build commands (#838) * cli: Reduce to test and build commands * Bump timeout * Fix test? * Fix * Fix output --- .github/workflows/_integration_test.yml | 32 +- libs/cli/examples/graphs_reqs_a/compose.yml | 5 - libs/cli/examples/graphs_reqs_b/compose.yml | 5 - libs/cli/langgraph_cli/cli.py | 386 ++++++-------------- libs/cli/langgraph_cli/docker.py | 117 +----- libs/cli/tests/unit_tests/agent.py | 81 ++++ libs/cli/tests/unit_tests/pipconfig.txt | 0 libs/cli/tests/unit_tests/test_cli.py | 111 ------ libs/cli/tests/unit_tests/test_config.json | 8 +- libs/cli/tests/unit_tests/test_config.py | 191 +--------- libs/cli/tests/unit_tests/test_docker.py | 101 ----- 11 files changed, 201 insertions(+), 836 deletions(-) delete mode 100644 libs/cli/examples/graphs_reqs_a/compose.yml delete mode 100644 libs/cli/examples/graphs_reqs_b/compose.yml create mode 100644 libs/cli/tests/unit_tests/pipconfig.txt delete mode 100644 libs/cli/tests/unit_tests/test_cli.py delete mode 100644 libs/cli/tests/unit_tests/test_docker.py diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml index fafe5da3f..20514eb00 100644 --- a/.github/workflows/_integration_test.yml +++ b/.github/workflows/_integration_test.yml @@ -30,7 +30,6 @@ jobs: with: python-version: ${{ matrix.python-version }} poetry-version: ${{ env.POETRY_VERSION }} - working-directory: libs/cli cache-key: integration-test-cli - name: Setup env if: steps.changed-files.outputs.all @@ -42,42 +41,19 @@ jobs: - name: Start service A if: steps.changed-files.outputs.all run: | - langgraph up -c examples/langgraph.json --wait --verbose - - name: Stop service A - if: steps.changed-files.outputs.all - run: | - langgraph down -c examples/langgraph.json - sudo rm -rf .langgraph-data + timeout 60 langgraph test -c examples/langgraph.json --verbose || (exit "$(($? == 124 ? 0 : $?))") - name: Start service B if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs run: | - langgraph up --wait --verbose - - name: Stop service B - if: steps.changed-files.outputs.all - working-directory: libs/cli/examples/graphs - run: | - langgraph down - sudo rm -rf .langgraph-data + timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))") - name: Start service C if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs_reqs_a run: | - langgraph up --wait -d compose.yml --verbose - - name: Stop service C - if: steps.changed-files.outputs.all - working-directory: libs/cli/examples/graphs_reqs_a - run: | - langgraph down - sudo rm -rf .langgraph-data + timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))") - name: Start service D if: steps.changed-files.outputs.all working-directory: libs/cli/examples/graphs_reqs_b run: | - langgraph up --wait -d compose.yml --verbose - - name: Stop service D - if: steps.changed-files.outputs.all - working-directory: libs/cli/examples/graphs_reqs_b - run: | - langgraph down - sudo rm -rf .langgraph-data + timeout 60 langgraph test --verbose || (exit "$(($? == 124 ? 0 : $?))") diff --git a/libs/cli/examples/graphs_reqs_a/compose.yml b/libs/cli/examples/graphs_reqs_a/compose.yml deleted file mode 100644 index 481bd842b..000000000 --- a/libs/cli/examples/graphs_reqs_a/compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - langgraph-api: - build: yoyoyo # invalid, will be overridden by generated compose file - environment: - HELLO: world diff --git a/libs/cli/examples/graphs_reqs_b/compose.yml b/libs/cli/examples/graphs_reqs_b/compose.yml deleted file mode 100644 index 481bd842b..000000000 --- a/libs/cli/examples/graphs_reqs_b/compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - langgraph-api: - build: yoyoyo # invalid, will be overridden by generated compose file - environment: - HELLO: world diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index a7a33f470..46d62bc15 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -1,8 +1,7 @@ import json import pathlib -import shutil import sys -from typing import Optional +from typing import Callable, Optional import click import click.exceptions @@ -10,24 +9,10 @@ import click.exceptions import langgraph_cli.config import langgraph_cli.docker from langgraph_cli.analytics import log_command -from langgraph_cli.config import Config from langgraph_cli.constants import DEFAULT_CONFIG, DEFAULT_PORT -from langgraph_cli.docker import DockerCapabilities from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.progress import Progress -OPT_DOCKER_COMPOSE = click.option( - "--docker-compose", - "-d", - help="Advanced: Path to docker-compose.yml file with additional services to launch.", - type=click.Path( - exists=True, - file_okay=True, - dir_okay=False, - resolve_path=True, - path_type=pathlib.Path, - ), -) OPT_CONFIG = click.option( "--config", "-c", @@ -104,12 +89,6 @@ OPT_PORT = click.option( \b """, ) -OPT_RECREATE = click.option( - "--recreate/--no-recreate", - default=False, - show_default=True, - help="Recreate containers even if their configuration and image haven't changed", -) OPT_PULL = click.option( "--pull/--no-pull", default=True, @@ -129,21 +108,6 @@ OPT_VERBOSE = click.option( default=False, help="Show more output from the server logs", ) -OPT_WATCH = click.option("--watch", is_flag=True, help="Restart on file changes") -OPT_LANGGRAPH_API_PATH = click.option( - "--langgraph-api-path", - type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), - hidden=True, -) -OPT_DEBUGGER_PORT = click.option( - "--debugger-port", - type=int, - help="Pull the debugger image locally and serve the UI on specified port", -) -OPT_POSTGRES_URI = click.option( - "--postgres-uri", - help="Postgres URI to use for the database. Defaults to launching a local database", -) @click.group() @@ -151,178 +115,147 @@ def cli(): pass -@OPT_RECREATE @OPT_PULL @OPT_PORT -@OPT_DOCKER_COMPOSE @OPT_CONFIG @OPT_VERBOSE -@OPT_DEBUGGER_PORT -@OPT_WATCH -@OPT_LANGGRAPH_API_PATH -@OPT_POSTGRES_URI -@click.option( - "--wait", - is_flag=True, - help="Wait for services to start before returning. Implies --detach", +@cli.command( + help="Start langgraph test server. This command enables you to confirm your graph will work inside the langgraph API server, before using LangGraph Cloud." ) -@cli.command(help="Start langgraph API server") @log_command -def up( +def test( config: pathlib.Path, - docker_compose: Optional[pathlib.Path], port: int, - recreate: bool, pull: bool, - watch: bool, - langgraph_api_path: Optional[pathlib.Path], - wait: bool, + # stop_when_ready: bool, verbose: bool, - debugger_port: Optional[int], - postgres_uri: Optional[str], ): with Runner() as runner, Progress(message="Pulling...") as set: + # check docker available capabilities = langgraph_cli.docker.check_capabilities(runner) - args, stdin = prepare( + # open config + with open(config) as f: + config_json = langgraph_cli.config.validate_config(json.load(f)) + # build + base_image = "langchain/langgraph-trial" + tag = f"langgraph-test-{config.parent.name}" + _build( runner, - capabilities=capabilities, - config_path=config, - docker_compose=docker_compose, - port=port, - pull=pull, - watch=watch, - langgraph_api_path=langgraph_api_path, - verbose=verbose, - debugger_port=debugger_port, - postgres_uri=postgres_uri, + set, + config, + config_json, + None, + base_image, + pull, + tag, ) - # add up + options - args.extend(["up", "--remove-orphans"]) - if recreate: - args.extend(["--force-recreate", "--renew-anon-volumes"]) - shutil.rmtree(config.parent / ".langgraph-data", ignore_errors=True) - try: - runner.run(subp_exec("docker", "volume", "rm", "langgraph-data")) - except click.exceptions.Exit: - pass - if watch: - args.append("--watch") - if wait: - args.append("--wait") + # run + 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: - args.append("--abort-on-container-exit") - # run docker compose - set("Building...") + 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", + ] + ) def on_stdout(line: str): - if "unpacking to docker.io" in line: - set("Starting...") - elif "GET /ok" in line: - debugger_origin = ( - f"http://localhost:{debugger_port}" - if debugger_port - else "https://smith.langchain.com" - ) + if "GET /ok" in line: set("") sys.stdout.write( f"""Ready! - API: http://localhost:{port} -- Docs: http://localhost:{port}/docs -- Debugger: {debugger_origin}/studio/?baseUrl=http://127.0.0.1:{port} """ ) sys.stdout.flush() return True - if capabilities.compose_type == "plugin": - compose_cmd = ["docker", "compose"] - elif capabilities.compose_type == "standalone": - compose_cmd = ["docker-compose"] - runner.run( subp_exec( - *compose_cmd, + "docker", *args, - input=stdin, + tag, verbose=verbose, on_stdout=on_stdout, ) ) -@OPT_PORT -@OPT_DOCKER_COMPOSE -@OPT_CONFIG -@OPT_VERBOSE -@OPT_DEBUGGER_PORT -@cli.command(help="Stop langgraph API server") -@log_command -def down( +def _build( + runner, + set: Callable[[str], None], config: pathlib.Path, - docker_compose: Optional[pathlib.Path], - port: int, - verbose: bool, - debugger_port: Optional[int], + config_json: dict, + platform: Optional[str], + base_image: Optional[str], + pull: bool, + tag: str, ): - with Runner() as runner: - capabilities = langgraph_cli.docker.check_capabilities(runner) - args, stdin = prepare( - runner, - capabilities=capabilities, - config_path=config, - docker_compose=docker_compose, - port=port, - pull=False, - watch=False, - langgraph_api_path=None, - verbose=verbose, - debugger_port=debugger_port, + base_image = base_image or "langchain/langgraph-api" + + # pull latest images + if pull: + runner.run( + subp_exec( + "docker", + "pull", + f"{base_image}:{config_json['python_version']}", + ) ) - # add down + options - args.append("down") - # run docker compose - if capabilities.compose_type == "plugin": - compose_cmd = ["docker", "compose"] - elif capabilities.compose_type == "standalone": - compose_cmd = ["docker-compose"] - - runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=verbose)) - - -@OPT_DOCKER_COMPOSE -@OPT_CONFIG -@click.option("--follow", "-f", is_flag=True, help="Follow logs") -@cli.command(help="Show langgraph API server logs") -@log_command -def logs( - config: pathlib.Path, - docker_compose: Optional[pathlib.Path], - follow: bool, -): - with Runner() as runner: - capabilities = langgraph_cli.docker.check_capabilities(runner) - args, stdin = prepare( - runner, - capabilities=capabilities, - config_path=config, - docker_compose=docker_compose, - port=8123, - pull=False, - watch=False, - verbose=False, - langgraph_api_path=None, + set("Building...") + # apply options + args = [ + "-f", + "-", # stdin + "-t", + tag, + ] + if platform: + args.extend(["--platform", platform]) + # apply config + stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image) + # run docker build + runner.run( + subp_exec( + "docker", "build", *args, str(config.parent), input=stdin, verbose=True ) - # add logs + options - args.append("logs") - if follow: - args.extend(["-f"]) - # run docker compose - if capabilities.compose_type == "plugin": - compose_cmd = ["docker", "compose"] - elif capabilities.compose_type == "standalone": - compose_cmd = ["docker-compose"] - - runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=True)) + ) @OPT_CONFIG @@ -363,114 +296,11 @@ def build( pull: bool, tag: str, ): - base_image = base_image or "langchain/langgraph-api" - with open(config) as f: - config_json = langgraph_cli.config.validate_config(json.load(f)) - with Runner() as runner: + with Runner() as runner, Progress(message="Pulling...") as set: # check docker available langgraph_cli.docker.check_capabilities(runner) - # pull latest images - if pull: - runner.run( - subp_exec( - "docker", - "pull", - f"{base_image}:{config_json['python_version']}", - ) - ) - # apply options - args = [ - "-f", - "-", # stdin - "-t", - tag, - ] - if platform: - args.extend(["--platform", platform]) - # apply config - stdin = langgraph_cli.config.config_to_docker(config, config_json, base_image) - # run docker build - runner.run( - subp_exec( - "docker", "build", *args, str(config.parent), input=stdin, verbose=True - ) - ) - - -def prepare_args_and_stdin( - *, - capabilities: DockerCapabilities, - config_path: pathlib.Path, - config: Config, - docker_compose: Optional[pathlib.Path], - port: int, - watch: bool, - langgraph_api_path: Optional[pathlib.Path], - debugger_port: Optional[int] = None, - postgres_uri: Optional[str] = None, -): - # prepare args - stdin = langgraph_cli.docker.compose( - capabilities, - port=port, - debugger_port=debugger_port, - postgres_uri=postgres_uri, - ) - args = [ - "--project-directory", - str(config_path.parent), - ] - # apply options - if docker_compose: - args.extend(["-f", str(docker_compose)]) - args.extend(["-f", "-"]) # stdin - # apply config - stdin += langgraph_cli.config.config_to_compose( - config_path, - config, - watch=watch, - langgraph_api_path=langgraph_api_path, - base_image="langchain/langgraph-api", - ) - return args, stdin - - -def prepare( - runner, - *, - capabilities: DockerCapabilities, - config_path: pathlib.Path, - docker_compose: Optional[pathlib.Path], - port: int, - pull: bool, - watch: bool, - langgraph_api_path: Optional[pathlib.Path], - verbose: bool, - debugger_port: Optional[int] = None, - postgres_uri: Optional[str] = None, -): - with open(config_path) as f: - config = langgraph_cli.config.validate_config(json.load(f)) - # pull latest images - if pull: - runner.run( - subp_exec( - "docker", - "pull", - f"langchain/langgraph-api:{config['python_version']}", - verbose=verbose, - ) - ) - - args, stdin = prepare_args_and_stdin( - capabilities=capabilities, - config_path=config_path, - config=config, - docker_compose=docker_compose, - port=port, - watch=watch, - langgraph_api_path=langgraph_api_path, - debugger_port=debugger_port, - postgres_uri=postgres_uri, - ) - return args, stdin + # open config + with open(config) as f: + config_json = langgraph_cli.config.validate_config(json.load(f)) + # build + _build(runner, set, config, config_json, platform, base_image, pull, tag) diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 1ea991a22..050859423 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -1,47 +1,13 @@ import json import pathlib import shutil -from typing import Literal, NamedTuple, Optional +from typing import NamedTuple import click.exceptions from langgraph_cli.exec import subp_exec ROOT = pathlib.Path(__file__).parent.resolve() -DEFAULT_POSTGRES_URI = ( - "postgres://postgres:postgres@langgraph-postgres:5432/postgres?sslmode=disable" -) - - -DB = """ - langgraph-postgres: - image: postgres:16 - ports: - - "5433:5432" - environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - langgraph-data:/var/lib/postgresql/data - healthcheck: - test: pg_isready -U postgres - start_period: 10s - timeout: 1s - retries: 5 -""" - - -DEBUGGER = """ - langgraph-debugger: - image: langchain/langgraph-debugger - restart: on-failure - ports: - - "{debugger_port}:3968" - depends_on: - langgraph-postgres: - condition: service_healthy -""" class Version(NamedTuple): @@ -50,14 +16,9 @@ class Version(NamedTuple): patch: int -DockerComposeType = Literal["plugin", "standalone"] - - class DockerCapabilities(NamedTuple): version_docker: Version - version_compose: Version healthcheck_start_interval: bool - compose_type: DockerComposeType = "plugin" def _parse_version(version: str) -> Version: @@ -88,87 +49,11 @@ def check_capabilities(runner) -> DockerCapabilities: if not info["ServerVersion"]: raise click.UsageError("Docker not running") from None - compose_type: DockerComposeType - try: - compose = next( - p for p in info["ClientInfo"]["Plugins"] if p["Name"] == "compose" - ) - compose_version_str = compose["Version"] - compose_type = "plugin" - except (KeyError, StopIteration): - if shutil.which("docker-compose") is None: - raise click.UsageError("Docker Compose not installed") from None - - compose_version_str, _ = runner.run( - subp_exec("docker-compose", "--version", "--short", collect=True) - ) - compose_type = "standalone" - # parse versions docker_version = _parse_version(info["ServerVersion"]) - compose_version = _parse_version(compose_version_str) # check capabilities return DockerCapabilities( version_docker=docker_version, - version_compose=compose_version, healthcheck_start_interval=docker_version >= Version(25, 0, 0), - compose_type=compose_type, ) - - -def compose( - capabilities: DockerCapabilities, - *, - port: int, - debugger_port: Optional[int] = None, - # postgres://user:password@host:port/database?option=value - postgres_uri: Optional[str] = None, -) -> str: - if postgres_uri is None: - include_db = True - postgres_uri = DEFAULT_POSTGRES_URI - else: - include_db = False - - db = DB.format() if include_db else "" - volumes = ( - """volumes: - langgraph-data: - driver: local -""" - if include_db - else "" - ) - if db: - if capabilities.healthcheck_start_interval: - db += """ - interval: 60s - start_interval: 1s""" - else: - db += """ - interval: 5s""" - - compose_str = f"""{volumes}services: -{db} -{DEBUGGER.format(debugger_port=debugger_port) if debugger_port else ""} - langgraph-api: - ports: - - "{port}:8000\"""" - if include_db: - compose_str += """ - depends_on: - langgraph-postgres: - condition: service_healthy""" - compose_str += f""" - environment: - POSTGRES_URI: {postgres_uri} -""" - if capabilities.healthcheck_start_interval: - compose_str += """ healthcheck: - test: python /api/healthcheck.py - interval: 60s - start_interval: 1s - start_period: 10s""" - - return compose_str diff --git a/libs/cli/tests/unit_tests/agent.py b/libs/cli/tests/unit_tests/agent.py index e69de29bb..e6c27c1f8 100644 --- a/libs/cli/tests/unit_tests/agent.py +++ b/libs/cli/tests/unit_tests/agent.py @@ -0,0 +1,81 @@ +import asyncio +import os +from typing import Annotated, Sequence, TypedDict + +from langchain_core.language_models.fake_chat_models import FakeListChatModel +from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage +from langgraph.graph import END, StateGraph, add_messages + +# check that env var is present +os.environ["SOME_ENV_VAR"] + + +class AgentState(TypedDict): + some_bytes: bytes + some_byte_array: bytearray + dict_with_bytes: dict[str, bytes] + messages: Annotated[Sequence[BaseMessage], add_messages] + sleep: int + + +async def call_model(state, config): + if sleep := state.get("sleep"): + await asyncio.sleep(sleep) + + messages = state["messages"] + + if len(messages) > 1: + assert state["some_bytes"] == b"some_bytes" + assert state["some_byte_array"] == bytearray(b"some_byte_array") + assert state["dict_with_bytes"] == {"more_bytes": b"more_bytes"} + + # hacky way to reset model to the "first" response + if isinstance(messages[-1], HumanMessage): + model.i = 0 + + response = await model.ainvoke(messages) + return { + "messages": [response], + "some_bytes": b"some_bytes", + "some_byte_array": bytearray(b"some_byte_array"), + "dict_with_bytes": {"more_bytes": b"more_bytes"}, + } + + +def call_tool(state): + last_message_content = state["messages"][-1].content + return { + "messages": [ + ToolMessage( + f"tool_call__{last_message_content}", tool_call_id="tool_call_id" + ) + ] + } + + +def should_continue(state): + messages = state["messages"] + last_message = messages[-1] + if last_message.content == "end": + return END + else: + return "tool" + + +# NOTE: the model cycles through responses infinitely here +model = FakeListChatModel(responses=["begin", "end"]) +workflow = StateGraph(AgentState) + +workflow.add_node("agent", call_model) +workflow.add_node("tool", call_tool) + +workflow.set_entry_point("agent") + +workflow.add_conditional_edges( + "agent", + should_continue, +) + +workflow.add_edge("tool", "agent") + +graph = workflow.compile() diff --git a/libs/cli/tests/unit_tests/pipconfig.txt b/libs/cli/tests/unit_tests/pipconfig.txt new file mode 100644 index 000000000..e69de29bb diff --git a/libs/cli/tests/unit_tests/test_cli.py b/libs/cli/tests/unit_tests/test_cli.py deleted file mode 100644 index 118074639..000000000 --- a/libs/cli/tests/unit_tests/test_cli.py +++ /dev/null @@ -1,111 +0,0 @@ -import pathlib - -from langgraph_cli.cli import prepare_args_and_stdin -from langgraph_cli.config import Config, validate_config -from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version -from langgraph_cli.util import clean_empty_lines - -DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( - version_docker=Version(26, 1, 1), - version_compose=Version(2, 27, 0), - healthcheck_start_interval=True, -) - - -def test_prepare_args_and_stdin(): - # this basically serves as an end-to-end test for using config and docker helpers - config_path = pathlib.Path("./langgraph.json") - config = validate_config( - Config(dependencies=["."], graphs={"agent": "agent.py:graph"}) - ) - port = 8000 - debugger_port = 8001 - - actual_args, actual_stdin = prepare_args_and_stdin( - capabilities=DEFAULT_DOCKER_CAPABILITIES, - config_path=config_path, - config=config, - docker_compose="custom-docker-compose.yml", - port=port, - debugger_port=debugger_port, - watch=True, - langgraph_api_path="path/to/langgraph-api", - ) - - expected_args = [ - "--project-directory", - ".", - "-f", - "custom-docker-compose.yml", - "-f", - "-", - ] - expected_stdin = f"""volumes: - langgraph-data: - driver: local -services: - langgraph-postgres: - image: postgres:16 - ports: - - "5433:5432" - environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - volumes: - - langgraph-data:/var/lib/postgresql/data - healthcheck: - test: pg_isready -U postgres - start_period: 10s - timeout: 1s - retries: 5 - interval: 60s - start_interval: 1s - langgraph-debugger: - image: langchain/langgraph-debugger - restart: on-failure - ports: - - "{debugger_port}:3968" - depends_on: - langgraph-postgres: - condition: service_healthy - langgraph-api: - ports: - - "8000:8000" - depends_on: - langgraph-postgres: - condition: service_healthy - environment: - POSTGRES_URI: {DEFAULT_POSTGRES_URI} - healthcheck: - test: python /api/healthcheck.py - interval: 60s - start_interval: 1s - start_period: 10s - - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api:3.11 - ADD . /deps/ - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}' - WORKDIR /deps/ - - develop: - watch: - - path: langgraph.json - action: rebuild - ignore: - - .langgraph-data - - path: . - action: rebuild - ignore: - - .langgraph-data - - path: path/to/langgraph-api - action: sync+restart - target: /api/langgraph_api\ -""" - assert actual_args == expected_args - assert clean_empty_lines(actual_stdin) == expected_stdin diff --git a/libs/cli/tests/unit_tests/test_config.json b/libs/cli/tests/unit_tests/test_config.json index b75d747c4..617c4d467 100644 --- a/libs/cli/tests/unit_tests/test_config.json +++ b/libs/cli/tests/unit_tests/test_config.json @@ -1,13 +1,15 @@ { "python_version": "3.12", "pip_config_file": "pipconfig.txt", - "dockerfile_lines": ["ARG meow"], + "dockerfile_lines": [ + "ARG meow" + ], "dependencies": [ "langchain_openai", "." ], "graphs": { - "agent": "tests/unit_tests/agent.py:graph" + "agent": "./agent.py:graph" }, "env": ".env" -} \ No newline at end of file +} diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index f50eee96c..ed16750cd 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -4,10 +4,10 @@ import pathlib import click import pytest -from langgraph_cli.config import config_to_compose, config_to_docker, validate_config +from langgraph_cli.config import config_to_docker, validate_config from langgraph_cli.util import clean_empty_lines -PATH_TO_CONFIG = pathlib.Path("tests/unit_tests/test_config.json") +PATH_TO_CONFIG = pathlib.Path(__file__).parent / "test_config.json" def test_validate_config(): @@ -230,190 +230,3 @@ RUN set -ex && \\ RUN PIP_CONFIG_FILE=/pipconfig.txt pip install -c /api/constraints.txt -e /deps/* ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph"}'""" assert clean_empty_lines(actual_docker_stdin) == expected_docker_stdin - - -# config_to_compose -def test_config_to_compose_simple_config(): - graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ - - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api:3.11 - ADD . /deps/__outer_unit_tests/unit_tests - RUN set -ex && \\ - for line in '[project]' \\ - 'name = "unit_tests"' \\ - 'version = "0.1"' \\ - '[tool.setuptools.package-data]' \\ - '"*" = ["**/*"]'; do \\ - echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ - done - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' - WORKDIR /deps/__outer_unit_tests/unit_tests - """ - actual_compose_stdin = config_to_compose( - PATH_TO_CONFIG, - validate_config({"dependencies": ["."], "graphs": graphs}), - "langchain/langgraph-api", - ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin - - -def test_config_to_compose_env_vars(): - graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """ OPENAI_API_KEY: key - - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api-custom:3.11 - ADD . /deps/__outer_unit_tests/unit_tests - RUN set -ex && \\ - for line in '[project]' \\ - 'name = "unit_tests"' \\ - 'version = "0.1"' \\ - '[tool.setuptools.package-data]' \\ - '"*" = ["**/*"]'; do \\ - echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ - done - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' - WORKDIR /deps/__outer_unit_tests/unit_tests - """ - openai_api_key = "key" - actual_compose_stdin = config_to_compose( - PATH_TO_CONFIG, - validate_config( - { - "dependencies": ["."], - "graphs": graphs, - "env": {"OPENAI_API_KEY": openai_api_key}, - } - ), - "langchain/langgraph-api-custom", - ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin - - -def test_config_to_compose_env_file(): - graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ - env_file: .env - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api:3.11 - ADD . /deps/__outer_unit_tests/unit_tests - RUN set -ex && \\ - for line in '[project]' \\ - 'name = "unit_tests"' \\ - 'version = "0.1"' \\ - '[tool.setuptools.package-data]' \\ - '"*" = ["**/*"]'; do \\ - echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ - done - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' - WORKDIR /deps/__outer_unit_tests/unit_tests - """ - actual_compose_stdin = config_to_compose( - PATH_TO_CONFIG, - validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}), - "langchain/langgraph-api", - ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin - - -def test_config_to_compose_watch(): - graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ - - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api:3.11 - ADD . /deps/__outer_unit_tests/unit_tests - RUN set -ex && \\ - for line in '[project]' \\ - 'name = "unit_tests"' \\ - 'version = "0.1"' \\ - '[tool.setuptools.package-data]' \\ - '"*" = ["**/*"]'; do \\ - echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ - done - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' - WORKDIR /deps/__outer_unit_tests/unit_tests - - develop: - watch: - - path: tests/unit_tests/test_config.json - action: rebuild - ignore: - - .langgraph-data - - path: tests/unit_tests - action: rebuild - ignore: - - .langgraph-data\ -""" - actual_compose_stdin = config_to_compose( - PATH_TO_CONFIG, - validate_config({"dependencies": ["."], "graphs": graphs}), - "langchain/langgraph-api", - watch=True, - ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin - - -def test_config_to_compose_end_to_end(): - # test all of the above + langgraph API path - graphs = {"agent": "./agent.py:graph"} - expected_compose_stdin = """\ - env_file: .env - pull_policy: build - build: - context: . - dockerfile_inline: | - FROM langchain/langgraph-api:3.11 - ADD . /deps/__outer_unit_tests/unit_tests - RUN set -ex && \\ - for line in '[project]' \\ - 'name = "unit_tests"' \\ - 'version = "0.1"' \\ - '[tool.setuptools.package-data]' \\ - '"*" = ["**/*"]'; do \\ - echo "$line" >> /deps/__outer_unit_tests/pyproject.toml; \\ - done - RUN pip install -c /api/constraints.txt -e /deps/* - ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_unit_tests/unit_tests/agent.py:graph"}' - WORKDIR /deps/__outer_unit_tests/unit_tests - - develop: - watch: - - path: tests/unit_tests/test_config.json - action: rebuild - ignore: - - .langgraph-data - - path: tests/unit_tests - action: rebuild - ignore: - - .langgraph-data - - path: path/to/langgraph/api - action: sync+restart - target: /api/langgraph_api\ -""" - actual_compose_stdin = config_to_compose( - PATH_TO_CONFIG, - validate_config({"dependencies": ["."], "graphs": graphs, "env": ".env"}), - "langchain/langgraph-api", - watch=True, - langgraph_api_path="path/to/langgraph/api", - ) - assert clean_empty_lines(actual_compose_stdin) == expected_compose_stdin diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py deleted file mode 100644 index 4d5e09b94..000000000 --- a/libs/cli/tests/unit_tests/test_docker.py +++ /dev/null @@ -1,101 +0,0 @@ -from langgraph_cli.docker import ( - DEFAULT_POSTGRES_URI, - DockerCapabilities, - Version, - compose, -) -from langgraph_cli.util import clean_empty_lines - -DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( - version_docker=Version(26, 1, 1), - version_compose=Version(2, 27, 0), - healthcheck_start_interval=False, -) - - -def test_compose_with_no_debugger_and_custom_db(): - port = 8123 - custom_postgres_uri = "custom_postgres_uri" - actual_compose_str = compose( - DEFAULT_DOCKER_CAPABILITIES, port=port, postgres_uri=custom_postgres_uri - ) - expected_compose_str = f"""services: - langgraph-api: - ports: - - "{port}:8000" - environment: - POSTGRES_URI: {custom_postgres_uri}""" - assert clean_empty_lines(actual_compose_str) == expected_compose_str - - -def test_compose_with_no_debugger_and_custom_db_with_healthcheck(): - port = 8123 - custom_postgres_uri = "custom_postgres_uri" - actual_compose_str = compose( - DEFAULT_DOCKER_CAPABILITIES._replace(healthcheck_start_interval=True), - port=port, - postgres_uri=custom_postgres_uri, - ) - expected_compose_str = f"""services: - langgraph-api: - ports: - - "{port}:8000" - environment: - POSTGRES_URI: {custom_postgres_uri} - healthcheck: - test: python /api/healthcheck.py - interval: 60s - start_interval: 1s - start_period: 10s""" - assert clean_empty_lines(actual_compose_str) == expected_compose_str - - -def test_compose_with_debugger_and_custom_db(): - port = 8123 - custom_postgres_uri = "custom_postgres_uri" - actual_compose_str = compose( - DEFAULT_DOCKER_CAPABILITIES, - port=port, - postgres_uri=custom_postgres_uri, - ) - expected_compose_str = f"""services: - langgraph-api: - ports: - - "{port}:8000" - environment: - POSTGRES_URI: {custom_postgres_uri}""" - assert clean_empty_lines(actual_compose_str) == expected_compose_str - - -def test_compose_with_debugger_and_default_db(): - port = 8123 - actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port) - expected_compose_str = f"""volumes: - langgraph-data: - driver: local -services: - langgraph-postgres: - image: postgres:16 - ports: - - "5433:5432" - environment: - POSTGRES_DB: postgres - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - 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-postgres: - condition: service_healthy - environment: - POSTGRES_URI: {DEFAULT_POSTGRES_URI}""" - assert clean_empty_lines(actual_compose_str) == expected_compose_str