cli: Reduce to test and build commands (#838)

* cli: Reduce to test and build commands

* Bump timeout

* Fix test?

* Fix

* Fix output
This commit is contained in:
Nuno Campos
2024-06-26 11:53:44 -07:00
committed by GitHub
parent fb5f3c972a
commit 5697f07163
11 changed files with 201 additions and 836 deletions
+4 -28
View File
@@ -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 : $?))")
@@ -1,5 +0,0 @@
services:
langgraph-api:
build: yoyoyo # invalid, will be overridden by generated compose file
environment:
HELLO: world
@@ -1,5 +0,0 @@
services:
langgraph-api:
build: yoyoyo # invalid, will be overridden by generated compose file
environment:
HELLO: world
+108 -278
View File
@@ -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)
+1 -116
View File
@@ -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
+81
View File
@@ -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()
-111
View File
@@ -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
+5 -3
View File
@@ -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"
}
}
+2 -189
View File
@@ -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
-101
View File
@@ -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