mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
sync changes
This commit is contained in:
+109
-25
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
def clean_empty_lines(input_str: str):
|
||||
return "\n".join(filter(None, input_str.splitlines()))
|
||||
@@ -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 <nuno@langchain.dev>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user