From e791f7ef5ae603fd7b95d7ab7a96b19f2d653d37 Mon Sep 17 00:00:00 2001 From: vbarda Date: Tue, 18 Jun 2024 20:19:55 -0400 Subject: [PATCH] sync changes --- libs/cli/langgraph_cli/cli.py | 134 ++++++++++++++++++----- libs/cli/langgraph_cli/docker.py | 14 ++- libs/cli/langgraph_cli/util.py | 2 + libs/cli/pyproject.toml | 2 +- libs/cli/tests/unit_tests/test_cli.py | 4 +- libs/cli/tests/unit_tests/test_config.py | 3 +- libs/cli/tests/unit_tests/test_docker.py | 12 +- libs/sdk-js/package.json | 2 +- libs/sdk-js/src/client.mts | 2 +- libs/sdk-py/langgraph_sdk/client.py | 69 ++++++++++++ 10 files changed, 200 insertions(+), 44 deletions(-) create mode 100644 libs/cli/langgraph_cli/util.py diff --git a/libs/cli/langgraph_cli/cli.py b/libs/cli/langgraph_cli/cli.py index 69d66c75d..94b1d8a6a 100644 --- a/libs/cli/langgraph_cli/cli.py +++ b/libs/cli/langgraph_cli/cli.py @@ -13,8 +13,9 @@ from langgraph_cli.config import Config from langgraph_cli.docker import DockerCapabilities from langgraph_cli.exec import Runner, subp_exec from langgraph_cli.progress import Progress +from langgraph_cli.util import clean_empty_lines -OPT_O = click.option( +OPT_DOCKER_COMPOSE = click.option( "--docker-compose", "-d", help="Advanced: Path to docker-compose.yml file with additional services to launch", @@ -26,7 +27,7 @@ OPT_O = click.option( path_type=pathlib.Path, ), ) -OPT_C = click.option( +OPT_CONFIG = click.option( "--config", "-c", help="""Path to configuration file declaring dependencies, graphs and environment variables. @@ -127,11 +128,21 @@ 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() @@ -142,16 +153,13 @@ def cli(): @OPT_RECREATE @OPT_PULL @OPT_PORT -@OPT_O -@OPT_C +@OPT_DOCKER_COMPOSE +@OPT_CONFIG @OPT_VERBOSE @OPT_DEBUGGER_PORT -@click.option("--watch", is_flag=True, help="Restart on file changes") -@click.option( - "--langgraph-api-path", - type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), - hidden=True, -) +@OPT_WATCH +@OPT_LANGGRAPH_API_PATH +@OPT_POSTGRES_URI @click.option( "--wait", is_flag=True, @@ -169,6 +177,7 @@ def up( wait: bool, verbose: bool, debugger_port: Optional[int], + postgres_uri: Optional[str], ): with Runner() as runner, Progress(message="Pulling...") as set: capabilities = langgraph_cli.docker.check_capabilities(runner) @@ -183,6 +192,7 @@ def up( langgraph_api_path=langgraph_api_path, verbose=verbose, debugger_port=debugger_port, + postgres_uri=postgres_uri, ) # add up + options args.extend(["up", "--remove-orphans"]) @@ -207,7 +217,7 @@ def up( debugger_origin = ( f"http://localhost:{debugger_port}" if debugger_port - else "https://dev.smith.langchain.com" + else "https://smith.langchain.com" ) set("") sys.stdout.write( @@ -237,8 +247,8 @@ def up( @OPT_PORT -@OPT_O -@OPT_C +@OPT_DOCKER_COMPOSE +@OPT_CONFIG @OPT_VERBOSE @OPT_DEBUGGER_PORT @cli.command(help="Stop langgraph API server") @@ -274,8 +284,8 @@ def down( runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=verbose)) -@OPT_O -@OPT_C +@OPT_DOCKER_COMPOSE +@OPT_CONFIG @click.option("--follow", "-f", is_flag=True, help="Follow logs") @cli.command(help="Show langgraph API server logs") def logs( @@ -309,7 +319,7 @@ def logs( runner.run(subp_exec(*compose_cmd, *args, input=stdin, verbose=True)) -@OPT_C +@OPT_CONFIG @OPT_PULL @click.option( "--tag", @@ -374,33 +384,101 @@ def build( ) +@cli.group(help="Export langgraph compose files") +def export(): + pass + + +@click.option( + "--output", + "-o", + help="Output path to write the docker compose file to", + type=click.Path( + exists=False, + file_okay=True, + dir_okay=False, + resolve_path=True, + path_type=pathlib.Path, + ), + required=True, +) +@OPT_CONFIG @OPT_PORT -@OPT_O -@OPT_C -@cli.command(help="Build a helm chart to deploy to a Kubernetes cluster", hidden=True) -def helm( +@OPT_WATCH +@OPT_LANGGRAPH_API_PATH +@export.command(name="compose", help="Export docker compose file") +def export_compose( + output: pathlib.Path, + config: pathlib.Path, + port: int, + watch: bool, + langgraph_api_path: Optional[pathlib.Path], +): + with Runner() as runner: + capabilities = langgraph_cli.docker.check_capabilities(runner) + _, stdin = prepare( + runner, + capabilities=capabilities, + config_path=config, + docker_compose=None, + pull=False, + watch=watch, + langgraph_api_path=langgraph_api_path, + port=port, + verbose=False, + ) + + with open(output, "w") as f: + f.write(clean_empty_lines(stdin)) + + +@click.option( + "--output", + "-o", + help="Output path (directory) to write the helm chart to", + type=click.Path( + exists=False, + file_okay=False, + dir_okay=True, + resolve_path=True, + path_type=pathlib.Path, + ), + required=True, +) +@OPT_PORT +@OPT_DOCKER_COMPOSE +@OPT_CONFIG +@export.command( + name="helm", + help="Build and export a helm chart to deploy to a Kubernetes cluster", + hidden=True, +) +def export_helm( + output: pathlib.Path, config: pathlib.Path, docker_compose: Optional[pathlib.Path], port: int, ): with open(config) as f: config_json = langgraph_cli.config.validate_config(json.load(f)) + with Runner() as runner: # check docker available capabilities = langgraph_cli.docker.check_capabilities(runner) # prepare args stdin = langgraph_cli.docker.compose(capabilities, port=port) args = [ + "convert", "--chart", - "-o=./helm", + "-o", + str(output), "-v", - "-f", - "-", # stdin ] # apply options if docker_compose: args.extend(["-f", str(docker_compose)]) - args.append("convert") + + args.extend(["-f", "-"]) # stdin # apply config stdin += langgraph_cli.config.config_to_compose(config, config_json) # run kompose convert @@ -417,10 +495,14 @@ def prepare_args_and_stdin( 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 + capabilities, + port=port, + debugger_port=debugger_port, + postgres_uri=postgres_uri, ) args = [ "--project-directory", @@ -449,6 +531,7 @@ def prepare( 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)) @@ -472,5 +555,6 @@ def prepare( watch=watch, langgraph_api_path=langgraph_api_path, debugger_port=debugger_port, + postgres_uri=postgres_uri, ) return args, stdin diff --git a/libs/cli/langgraph_cli/docker.py b/libs/cli/langgraph_cli/docker.py index 5266ef316..d7627015f 100644 --- a/libs/cli/langgraph_cli/docker.py +++ b/libs/cli/langgraph_cli/docker.py @@ -77,12 +77,18 @@ def _parse_version(version: str) -> Version: def check_capabilities(runner) -> DockerCapabilities: # check docker available + if shutil.which("docker") is None: + raise click.UsageError("Docker not installed") from None + try: stdout, _ = runner.run(subp_exec("docker", "info", "-f", "json", collect=True)) info = json.loads(stdout) except (click.exceptions.Exit, json.JSONDecodeError): raise click.UsageError("Docker not installed or not running") from None + if not info["ServerVersion"]: + raise click.UsageError("Docker not running") from None + compose_type: DockerComposeType try: compose = next( @@ -146,15 +152,19 @@ def compose( langgraph-api: restart: on-failure ports: - - "{port}:8000" + - "{port}:8000\"""" + if include_db: + compose_str += """ depends_on: langgraph-postgres: - condition: service_healthy + 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""" diff --git a/libs/cli/langgraph_cli/util.py b/libs/cli/langgraph_cli/util.py new file mode 100644 index 000000000..79b67a2c9 --- /dev/null +++ b/libs/cli/langgraph_cli/util.py @@ -0,0 +1,2 @@ +def clean_empty_lines(input_str: str): + return "\n".join(filter(None, input_str.splitlines())) diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index dca1782cb..0c67c03ee 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-cli" -version = "0.1.35" +version = "0.1.37" description = "CLI for interacting with LangGraph API" authors = ["Nuno Campos "] readme = "README.md" diff --git a/libs/cli/tests/unit_tests/test_cli.py b/libs/cli/tests/unit_tests/test_cli.py index 44f63bb2a..4d605443e 100644 --- a/libs/cli/tests/unit_tests/test_cli.py +++ b/libs/cli/tests/unit_tests/test_cli.py @@ -3,8 +3,7 @@ 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 .helpers import clean_empty_lines +from langgraph_cli.util import clean_empty_lines DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( version_docker=Version(26, 1, 1), @@ -81,6 +80,7 @@ services: environment: POSTGRES_URI: {DEFAULT_POSTGRES_URI} healthcheck: + test: python /api/healthcheck.py interval: 60s start_interval: 1s start_period: 10s diff --git a/libs/cli/tests/unit_tests/test_config.py b/libs/cli/tests/unit_tests/test_config.py index 9adcbd071..9d61a29c7 100644 --- a/libs/cli/tests/unit_tests/test_config.py +++ b/libs/cli/tests/unit_tests/test_config.py @@ -5,8 +5,7 @@ import click import pytest from langgraph_cli.config import config_to_compose, config_to_docker, validate_config - -from .helpers import clean_empty_lines +from langgraph_cli.util import clean_empty_lines PATH_TO_CONFIG = pathlib.Path("tests/unit_tests/test_config.json") diff --git a/libs/cli/tests/unit_tests/test_docker.py b/libs/cli/tests/unit_tests/test_docker.py index 425d97123..416c22889 100644 --- a/libs/cli/tests/unit_tests/test_docker.py +++ b/libs/cli/tests/unit_tests/test_docker.py @@ -4,7 +4,7 @@ from langgraph_cli.docker import ( Version, compose, ) -from tests.unit_tests.helpers import clean_empty_lines +from langgraph_cli.util import clean_empty_lines DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities( version_docker=Version(26, 1, 1), @@ -24,9 +24,6 @@ def test_compose_with_no_debugger_and_custom_db(): restart: on-failure ports: - "{port}:8000" - depends_on: - langgraph-postgres: - condition: service_healthy environment: POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str @@ -45,12 +42,10 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck(): restart: on-failure ports: - "{port}:8000" - depends_on: - langgraph-postgres: - condition: service_healthy environment: POSTGRES_URI: {custom_postgres_uri} healthcheck: + test: python /api/healthcheck.py interval: 60s start_interval: 1s start_period: 10s""" @@ -70,9 +65,6 @@ def test_compose_with_debugger_and_custom_db(): restart: on-failure ports: - "{port}:8000" - depends_on: - langgraph-postgres: - condition: service_healthy environment: POSTGRES_URI: {custom_postgres_uri}""" assert clean_empty_lines(actual_compose_str) == expected_compose_str diff --git a/libs/sdk-js/package.json b/libs/sdk-js/package.json index fd7ee23e3..6edf725d1 100644 --- a/libs/sdk-js/package.json +++ b/libs/sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/langgraph-sdk", - "version": "0.0.1-rc.11", + "version": "0.0.1-rc.12", "description": "Client library for interacting with the LangGraph API", "type": "module", "packageManager": "yarn@1.22.19", diff --git a/libs/sdk-js/src/client.mts b/libs/sdk-js/src/client.mts index 6e49af778..7d2fd1472 100644 --- a/libs/sdk-js/src/client.mts +++ b/libs/sdk-js/src/client.mts @@ -97,7 +97,7 @@ class BaseClient { const response = await this.asyncCaller.fetch( ...this.prepareFetchOptions(path, options), ); - if (response.status == 202) { + if (response.status === 202 || response.status === 204) { return undefined as T; } return response.json() as T; diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 8c753f3ab..bfdf9a28a 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -67,6 +67,7 @@ class LangGraphClient: self.assistants = AssistantsClient(self.http) self.threads = ThreadsClient(self.http) self.runs = RunsClient(self.http) + self.crons = CronClient(self.http) class HttpClient: @@ -620,6 +621,74 @@ class RunsClient: await self.http.delete(f"/threads/{thread_id}/runs/{run_id}") +class CronClient: + def __init__(self, http_client: HttpClient) -> None: + self.http = http_client + + async def create_for_thread( + self, + thread_id: str, + assistant_id: str, + *, + schedule: str, + input: Optional[dict] = None, + metadata: Optional[dict] = None, + config: Optional[Config] = None, + interrupt_before: Optional[list[str]] = None, + interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + multitask_strategy: Optional[str] = None, + ) -> Run: + """Create a background run.""" + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + } + if multitask_strategy: + payload["multitask_strategy"] = multitask_strategy + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.post(f"/threads/{thread_id}/runs/crons", json=payload) + + async def create( + self, + assistant_id: str, + *, + schedule: str, + input: Optional[dict] = None, + metadata: Optional[dict] = None, + config: Optional[Config] = None, + interrupt_before: Optional[list[str]] = None, + interrupt_after: Optional[list[str]] = None, + webhook: Optional[str] = None, + multitask_strategy: Optional[str] = None, + ) -> Run: + """Create a background run.""" + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + } + if multitask_strategy: + payload["multitask_strategy"] = multitask_strategy + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.post("/runs/crons", json=payload) + + async def delete(self, cron_id: str) -> None: + """Delete a cron.""" + await self.http.delete(f"/runs/crons/{cron_id}") + + def _get_api_key(api_key: Optional[str] = None) -> Optional[str]: """Get the API key from the environment. Precedence: