cli[minor]: Add langgraph new command (#2369)

Adds a "new" command to create langgraph application from a template.
This commit is contained in:
Eugene Yurtsev
2024-11-12 14:30:51 -05:00
committed by GitHub
parent a73f9affab
commit 2bcf1c0a20
9 changed files with 337 additions and 2 deletions
+133
View File
@@ -0,0 +1,133 @@
import pathlib
from click.testing import CliRunner
from langgraph_cli.cli import cli, 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
debugger_graph_url = f"http://127.0.0.1:{port}"
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,
debugger_base_url=debugger_graph_url,
watch=True,
)
expected_args = [
"--project-directory",
".",
"-f",
"custom-docker-compose.yml",
"-f",
"-",
]
expected_stdin = 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: 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
depends_on:
langgraph-postgres:
condition: service_healthy
ports:
- "{debugger_port}:3968"
environment:
VITE_STUDIO_LOCAL_GRAPH_URL: {debugger_graph_url}
langgraph-api:
ports:
- "8000: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}
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 PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/*
ENV LANGSERVE_GRAPHS='{{"agent": "agent.py:graph"}}'
WORKDIR /deps/
develop:
watch:
- path: langgraph.json
action: rebuild
- path: .
action: rebuild\
"""
assert actual_args == expected_args
assert clean_empty_lines(actual_stdin) == expected_stdin
def test_version_option() -> None:
"""Test the --version option of the CLI."""
runner = CliRunner()
result = runner.invoke(cli, ["--version"])
# Verify that the command executed successfully
assert result.exit_code == 0, "Expected exit code 0 for --version option"
# Check that the output contains the correct version information
assert (
"LangGraph CLI, version" in result.output
), "Expected version information in output"
@@ -0,0 +1,70 @@
"""Unit tests for the 'new' CLI command.
This command creates a new LangGraph project using a specified template.
"""
import os
from io import BytesIO
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch
from urllib import request
from zipfile import ZipFile
from click.testing import CliRunner
from langgraph_cli.cli import cli
from langgraph_cli.templates import TEMPLATE_ID_TO_CONFIG
@patch.object(request, "urlopen")
def test_create_new_with_mocked_download(mock_urlopen: MagicMock) -> None:
"""Test the 'new' CLI command with a mocked download response using urllib."""
# Mock the response content to simulate a ZIP file
mock_zip_content = BytesIO()
with ZipFile(mock_zip_content, "w") as mock_zip:
mock_zip.writestr("test-file.txt", "Test content.")
# Create a mock response that behaves like a context manager
mock_response = MagicMock()
mock_response.read.return_value = mock_zip_content.getvalue()
mock_response.__enter__.return_value = mock_response # Setup enter context
mock_response.status = 200
mock_urlopen.return_value = mock_response
with TemporaryDirectory() as temp_dir:
runner = CliRunner()
template = next(
iter(TEMPLATE_ID_TO_CONFIG)
) # Select the first template for the test
result = runner.invoke(cli, ["new", temp_dir, "--template", template])
# Verify CLI command execution and success
assert result.exit_code == 0, result.output
assert (
"New project created" in result.output
), "Expected success message in output."
# Verify that the directory is not empty
assert os.listdir(temp_dir), "Expected files to be created in temp directory."
# Check for a known file in the extracted content
extracted_files = [f.name for f in Path(temp_dir).glob("*")]
assert (
"test-file.txt" in extracted_files
), "Expected 'test-file.txt' in the extracted content."
def test_invalid_template_id() -> None:
"""Test that an invalid template ID passed via CLI results in a graceful error."""
runner = CliRunner()
result = runner.invoke(
cli, ["new", "dummy_path", "--template", "invalid-template-id"]
)
# Verify the command failed and proper message is displayed
assert result.exit_code != 0, "Expected non-zero exit code for invalid template."
assert (
"Template 'invalid-template-id' not found" in result.output
), "Expected error message in output."