mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-11-10 07:57:43 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae41b5ad2 | ||
|
|
428ebe1a01 | ||
|
|
8e05912899 | ||
|
|
f899af1c73 | ||
|
|
5f479f1af5 | ||
|
|
33c3edfde2 | ||
|
|
292fd5787c | ||
|
|
b904db211c | ||
|
|
ecee22fa16 |
No files matched your search
@@ -32,8 +32,6 @@ def test(config: pathlib.Path, port: int, tag: str, verbose: bool):
|
||||
docker_compose=None,
|
||||
port=port,
|
||||
watch=False,
|
||||
debugger_port=None,
|
||||
debugger_base_url=f"http://127.0.0.1:{port}",
|
||||
postgres_uri=None,
|
||||
api_version=None,
|
||||
image=tag,
|
||||
@@ -173,5 +171,5 @@ if __name__ == "__main__":
|
||||
except BaseException:
|
||||
logger.exception("Test failed")
|
||||
raise
|
||||
|
||||
|
||||
logger.info("Test execution finished")
|
||||
+9
-14
@@ -129,9 +129,8 @@ The top level includes `deployment_id`, `start_time`, `end_time`, and
|
||||
point bounds. The CLI resolves omitted bounds once: end defaults to now (UTC),
|
||||
and start defaults to 15 minutes before the resolved end. With only `--end-time`,
|
||||
start is 15 minutes before that value; with only `--start-time`, end is now. Both
|
||||
values are sent explicitly to the backend and included in default compact output,
|
||||
even when there are no points. `step_seconds` is the most common gap between
|
||||
consecutive timestamps within series, with ties broken by the smallest gap. Whole seconds
|
||||
values are sent explicitly to the backend and output, even when there are no points. `step_seconds` is the most common gap between consecutive
|
||||
timestamps within series, with ties broken by the smallest gap. Whole seconds
|
||||
are integers; fractional seconds are preserved. It is `null` only when every
|
||||
series has fewer than two timestamps. UI metadata and repeated series IDs are
|
||||
omitted. The host-backend response itself is unchanged.
|
||||
@@ -141,20 +140,16 @@ langgraph deploy metrics DEPLOYMENT_ID --summary
|
||||
langgraph deploy metrics DEPLOYMENT_ID --raw
|
||||
```
|
||||
|
||||
`--summary` requests server-side aggregation with `summary=true` and prints the
|
||||
response without reshaping or rounding. It includes `deployment_id`,
|
||||
`step_seconds`, and `sections`, but no `start_time`, `end_time`, or `points`.
|
||||
Series contain `metric`, `unit`, `latest`, `min`, `max`, and `avg`; units include
|
||||
`percent`, `count`, `milliseconds`, and `responses_per_second`.
|
||||
`--raw` returns the original UI chart schema without reshaping or rounding.
|
||||
These flags are mutually exclusive. Neither mode returns traces.
|
||||
`--summary` returns the same shape without `points`. `--raw` returns the original
|
||||
UI chart schema without reshaping or rounding. These flags are mutually exclusive.
|
||||
Neither mode returns traces.
|
||||
|
||||
Size comparison using the realistic synthetic unit-test fixture (compact JSON
|
||||
serialization; not a live deployment benchmark):
|
||||
serialization for all modes; not a live deployment benchmark):
|
||||
|
||||
| Before: raw UI schema | After: default |
|
||||
| --- | --- |
|
||||
| 4,302 bytes | 1,226 bytes (72% smaller) |
|
||||
| Before: raw UI schema | After: default | After: summary |
|
||||
| --- | --- | --- |
|
||||
| 4,302 bytes | 1,226 bytes (72% smaller) | 806 bytes (81% smaller) |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -48,9 +48,6 @@ def get_anonymized_params(
|
||||
if kwargs.get("docker_compose"):
|
||||
params["docker_compose"] = True
|
||||
|
||||
if kwargs.get("debugger_port"):
|
||||
params["debugger_port"] = True
|
||||
|
||||
if kwargs.get("postgres_uri"):
|
||||
params["postgres_uri"] = True
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from urllib.parse import SplitResult, urlencode, urlsplit, urlunsplit
|
||||
|
||||
import click
|
||||
import click.exceptions
|
||||
@@ -140,17 +141,6 @@ OPT_VERBOSE = click.option(
|
||||
help="Show more output from the server logs",
|
||||
)
|
||||
OPT_WATCH = click.option("--watch", is_flag=True, help="Restart on file changes")
|
||||
OPT_DEBUGGER_PORT = click.option(
|
||||
"--debugger-port",
|
||||
type=int,
|
||||
help="Pull the debugger image locally and serve the UI on specified port",
|
||||
)
|
||||
OPT_DEBUGGER_BASE_URL = click.option(
|
||||
"--debugger-base-url",
|
||||
type=str,
|
||||
help="URL used by the debugger to access LangGraph API. Defaults to http://127.0.0.1:[PORT]",
|
||||
)
|
||||
|
||||
OPT_POSTGRES_URI = click.option(
|
||||
"--postgres-uri",
|
||||
help="Postgres URI to use for the database. Defaults to launching a local database",
|
||||
@@ -242,18 +232,94 @@ cli.add_command(deploy)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _validated_http_url(value: str, option_name: str) -> SplitResult:
|
||||
try:
|
||||
parsed = urlsplit(value)
|
||||
hostname = parsed.hostname
|
||||
_ = parsed.port
|
||||
except ValueError as exc:
|
||||
raise click.UsageError(
|
||||
f"{option_name} must be a valid HTTP(S) URL without credentials."
|
||||
) from exc
|
||||
|
||||
if (
|
||||
value != value.strip()
|
||||
or parsed.scheme not in {"http", "https"}
|
||||
or not parsed.netloc
|
||||
or not hostname
|
||||
or parsed.username is not None
|
||||
or parsed.password is not None
|
||||
):
|
||||
raise click.UsageError(
|
||||
f"{option_name} must be a valid HTTP(S) URL without credentials."
|
||||
)
|
||||
return parsed
|
||||
|
||||
|
||||
def _studio_link(
|
||||
*,
|
||||
port: int,
|
||||
studio_url: str | None,
|
||||
api_url: str | None,
|
||||
debugger_base_url: str | None,
|
||||
) -> str:
|
||||
if debugger_base_url is not None:
|
||||
if api_url is not None and api_url != debugger_base_url:
|
||||
raise click.UsageError(
|
||||
"--api-url and --debugger-base-url cannot specify different URLs."
|
||||
)
|
||||
click.echo(
|
||||
"Warning: --debugger-base-url is deprecated; use --api-url instead.",
|
||||
err=True,
|
||||
)
|
||||
api_url = debugger_base_url
|
||||
|
||||
studio_url = "https://smith.langchain.com" if studio_url is None else studio_url
|
||||
api_url = f"http://127.0.0.1:{port}" if api_url is None else api_url
|
||||
studio_parts = _validated_http_url(studio_url, "--studio-url")
|
||||
_validated_http_url(api_url, "--api-url")
|
||||
if studio_parts.query or studio_parts.fragment:
|
||||
raise click.UsageError(
|
||||
"--studio-url must not include a query string or fragment."
|
||||
)
|
||||
|
||||
studio_path = f"{studio_parts.path.rstrip('/')}/studio/"
|
||||
return urlunsplit(
|
||||
studio_parts._replace(
|
||||
path=studio_path,
|
||||
query=urlencode({"baseUrl": api_url}),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@OPT_RECREATE
|
||||
@OPT_PULL
|
||||
@OPT_PORT
|
||||
@OPT_DOCKER_COMPOSE
|
||||
@OPT_CONFIG
|
||||
@OPT_VERBOSE
|
||||
@OPT_DEBUGGER_PORT
|
||||
@OPT_DEBUGGER_BASE_URL
|
||||
@OPT_WATCH
|
||||
@OPT_POSTGRES_URI
|
||||
@OPT_API_VERSION
|
||||
@OPT_ENGINE_RUNTIME_MODE
|
||||
@click.option(
|
||||
"--studio-url",
|
||||
type=str,
|
||||
default=None,
|
||||
help="URL of the LangGraph Studio instance. Defaults to https://smith.langchain.com",
|
||||
)
|
||||
@click.option(
|
||||
"--api-url",
|
||||
type=str,
|
||||
default=None,
|
||||
help="URL that LangGraph Studio uses to access the API. Defaults to http://127.0.0.1:[PORT]",
|
||||
)
|
||||
@click.option(
|
||||
"--debugger-base-url",
|
||||
type=str,
|
||||
default=None,
|
||||
hidden=True,
|
||||
)
|
||||
@click.option(
|
||||
"--image",
|
||||
type=str,
|
||||
@@ -284,14 +350,21 @@ def up(
|
||||
watch: bool,
|
||||
wait: bool,
|
||||
verbose: bool,
|
||||
debugger_port: int | None,
|
||||
debugger_base_url: str | None,
|
||||
postgres_uri: str | None,
|
||||
api_version: str | None,
|
||||
engine_runtime_mode: str,
|
||||
studio_url: str | None,
|
||||
api_url: str | None,
|
||||
debugger_base_url: str | None,
|
||||
image: str | None,
|
||||
base_image: str | None,
|
||||
):
|
||||
studio_link = _studio_link(
|
||||
port=port,
|
||||
studio_url=studio_url,
|
||||
api_url=api_url,
|
||||
debugger_base_url=debugger_base_url,
|
||||
)
|
||||
click.secho("Starting LangGraph API server...", fg="green")
|
||||
click.secho(
|
||||
"""For local dev, requires env var LANGSMITH_API_KEY with access to LangSmith Deployment.
|
||||
@@ -308,8 +381,6 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
pull=pull,
|
||||
watch=watch,
|
||||
verbose=verbose,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
api_version=api_version,
|
||||
engine_runtime_mode=engine_runtime_mode,
|
||||
@@ -337,20 +408,12 @@ For production use, requires a license key in env var LANGGRAPH_CLOUD_LICENSE_KE
|
||||
if "unpacking to docker.io" in line:
|
||||
set("Starting...")
|
||||
elif "Application startup complete" in line:
|
||||
debugger_origin = (
|
||||
f"http://localhost:{debugger_port}"
|
||||
if debugger_port
|
||||
else "https://smith.langchain.com"
|
||||
)
|
||||
debugger_base_url_query = (
|
||||
debugger_base_url or f"http://127.0.0.1:{port}"
|
||||
)
|
||||
set("")
|
||||
sys.stdout.write(
|
||||
f"""Ready!
|
||||
- API: http://localhost:{port}
|
||||
- Docs: http://localhost:{port}/docs
|
||||
- LangGraph Studio: {debugger_origin}/studio/?baseUrl={debugger_base_url_query}
|
||||
- LangGraph Studio: {studio_link}
|
||||
"""
|
||||
)
|
||||
sys.stdout.flush()
|
||||
@@ -936,8 +999,6 @@ def prepare_args_and_stdin(
|
||||
docker_compose: pathlib.Path | None,
|
||||
port: int,
|
||||
watch: bool,
|
||||
debugger_port: int | None = None,
|
||||
debugger_base_url: str | None = None,
|
||||
postgres_uri: str | None = None,
|
||||
api_version: str | None = None,
|
||||
engine_runtime_mode: str = "combined_queue_worker",
|
||||
@@ -951,8 +1012,6 @@ def prepare_args_and_stdin(
|
||||
stdin = langgraph_cli.docker.compose(
|
||||
capabilities,
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
@@ -990,8 +1049,6 @@ def prepare(
|
||||
pull: bool,
|
||||
watch: bool,
|
||||
verbose: bool,
|
||||
debugger_port: int | None = None,
|
||||
debugger_base_url: str | None = None,
|
||||
postgres_uri: str | None = None,
|
||||
api_version: str | None = None,
|
||||
engine_runtime_mode: str = "combined_queue_worker",
|
||||
@@ -1033,8 +1090,6 @@ def prepare(
|
||||
docker_compose=docker_compose,
|
||||
port=port,
|
||||
watch=watch,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url or f"http://127.0.0.1:{port}",
|
||||
postgres_uri=postgres_uri,
|
||||
api_version=api_version,
|
||||
engine_runtime_mode=engine_runtime_mode,
|
||||
|
||||
@@ -2694,20 +2694,19 @@ def deploy_metrics(
|
||||
}.items()
|
||||
if value is not None
|
||||
}
|
||||
if summary:
|
||||
params["summary"] = "true"
|
||||
client = _create_host_backend_client(host_url, api_key, no_input=True)
|
||||
result = _call_host_backend_with_optional_tenant(
|
||||
client,
|
||||
lambda c: c.get_deployment_metrics(str(deployment_id), params),
|
||||
no_input=True,
|
||||
)
|
||||
if not raw and not summary:
|
||||
if not raw:
|
||||
result = compact_deployment_metrics(
|
||||
result,
|
||||
str(deployment_id),
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
summary=summary,
|
||||
)
|
||||
click.echo(json_mod.dumps(result, separators=(",", ":")))
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ def compact_deployment_metrics(
|
||||
*,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
summary: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Reshape UI charts without mutating the response; step is in seconds."""
|
||||
sections = {}
|
||||
@@ -73,13 +74,14 @@ def compact_deployment_metrics(
|
||||
"max": round(max(values), 3) if values else None,
|
||||
"avg": round(fmean(values), 3) if values else None,
|
||||
}
|
||||
stats["points"] = [
|
||||
[
|
||||
_iso(timestamp),
|
||||
round(value, 3) if value is not None else None,
|
||||
if not summary:
|
||||
stats["points"] = [
|
||||
[
|
||||
_iso(timestamp),
|
||||
round(value, 3) if value is not None else None,
|
||||
]
|
||||
for timestamp, value in points
|
||||
]
|
||||
for timestamp, value in points
|
||||
]
|
||||
series_output[series["name"]] = stats
|
||||
charts[chart["title"]] = series_output or {"_empty": True}
|
||||
sections[section["title"]] = charts or {"_empty": True}
|
||||
|
||||
@@ -142,29 +142,6 @@ def check_capabilities(runner) -> DockerCapabilities:
|
||||
)
|
||||
|
||||
|
||||
def debugger_compose(*, port: int | None = None, base_url: str | None = None) -> dict:
|
||||
if port is None:
|
||||
return ""
|
||||
|
||||
config = {
|
||||
"langgraph-debugger": {
|
||||
"image": "langchain/langgraph-debugger",
|
||||
"restart": "on-failure",
|
||||
"depends_on": {
|
||||
"langgraph-postgres": {"condition": "service_healthy"},
|
||||
},
|
||||
"ports": [f'"{port}:3968"'],
|
||||
}
|
||||
}
|
||||
|
||||
if base_url:
|
||||
config["langgraph-debugger"]["environment"] = {
|
||||
"VITE_STUDIO_LOCAL_GRAPH_URL": base_url
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# Function to convert dictionary to YAML
|
||||
def dict_to_yaml(d: dict, *, indent: int = 0) -> str:
|
||||
"""Convert a dictionary to a YAML string."""
|
||||
@@ -191,8 +168,6 @@ def compose_as_dict(
|
||||
capabilities: DockerCapabilities,
|
||||
*,
|
||||
port: int,
|
||||
debugger_port: int | None = None,
|
||||
debugger_base_url: str | None = None,
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: str | None = None,
|
||||
# If you are running against an already-built image, you can pass it here
|
||||
@@ -253,12 +228,6 @@ def compose_as_dict(
|
||||
else:
|
||||
services["langgraph-postgres"]["healthcheck"]["interval"] = "5s"
|
||||
|
||||
# Add optional debugger service if debugger_port is specified
|
||||
if debugger_port:
|
||||
services["langgraph-debugger"] = debugger_compose(
|
||||
port=debugger_port, base_url=debugger_base_url
|
||||
)["langgraph-debugger"]
|
||||
|
||||
# Add langgraph-api service
|
||||
api_environment = {
|
||||
"REDIS_URI": "redis://langgraph-redis:6379",
|
||||
@@ -289,7 +258,7 @@ def compose_as_dict(
|
||||
"test": "python /api/healthcheck.py",
|
||||
"interval": "60s",
|
||||
"start_interval": "1s",
|
||||
"start_period": "10s",
|
||||
"start_period": "60s",
|
||||
}
|
||||
|
||||
# Final compose dictionary with volumes included if needed
|
||||
@@ -305,8 +274,6 @@ def compose(
|
||||
capabilities: DockerCapabilities,
|
||||
*,
|
||||
port: int,
|
||||
debugger_port: int | None = None,
|
||||
debugger_base_url: str | None = None,
|
||||
# postgres://user:password@host:port/database?option=value
|
||||
postgres_uri: str | None = None,
|
||||
image: str | None = None,
|
||||
@@ -318,8 +285,6 @@ def compose(
|
||||
compose_content = compose_as_dict(
|
||||
capabilities,
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_base_url,
|
||||
postgres_uri=postgres_uri,
|
||||
image=image,
|
||||
base_image=base_image,
|
||||
|
||||
@@ -10,10 +10,11 @@ from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import click
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
import langgraph_cli.deploy as deploy_module
|
||||
from langgraph_cli.cli import cli, prepare_args_and_stdin
|
||||
from langgraph_cli.cli import _studio_link, cli, prepare_args_and_stdin
|
||||
from langgraph_cli.config import Config, _get_pip_cleanup_lines, validate_config
|
||||
from langgraph_cli.docker import DEFAULT_POSTGRES_URI, DockerCapabilities, Version
|
||||
from langgraph_cli.host_backend import HostBackendError
|
||||
@@ -59,8 +60,6 @@ def test_prepare_args_and_stdin() -> None:
|
||||
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,
|
||||
@@ -68,8 +67,6 @@ def test_prepare_args_and_stdin() -> None:
|
||||
config=config,
|
||||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_graph_url,
|
||||
watch=True,
|
||||
)
|
||||
|
||||
@@ -113,16 +110,6 @@ services:
|
||||
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"
|
||||
@@ -138,7 +125,7 @@ services:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
start_period: 60s
|
||||
|
||||
pull_policy: build
|
||||
build:
|
||||
@@ -181,8 +168,6 @@ def test_prepare_args_and_stdin_with_image() -> None:
|
||||
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,
|
||||
@@ -190,8 +175,6 @@ def test_prepare_args_and_stdin_with_image() -> None:
|
||||
config=config,
|
||||
docker_compose=pathlib.Path("custom-docker-compose.yml"),
|
||||
port=port,
|
||||
debugger_port=debugger_port,
|
||||
debugger_base_url=debugger_graph_url,
|
||||
watch=True,
|
||||
image="my-cool-image",
|
||||
)
|
||||
@@ -236,16 +219,6 @@ services:
|
||||
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"
|
||||
@@ -262,7 +235,7 @@ services:
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s
|
||||
start_period: 60s
|
||||
|
||||
|
||||
develop:
|
||||
@@ -292,6 +265,82 @@ def test_version_option() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_up_help_shows_hosted_studio_options() -> None:
|
||||
result = CliRunner().invoke(cli, ["up", "--help"])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "--studio-url" in result.output
|
||||
assert "--api-url" in result.output
|
||||
assert "--debugger-port" not in result.output
|
||||
assert "--debugger-base-url" not in result.output
|
||||
|
||||
|
||||
def test_studio_link_defaults_to_hosted_studio() -> None:
|
||||
assert _studio_link(
|
||||
port=8123,
|
||||
studio_url=None,
|
||||
api_url=None,
|
||||
debugger_base_url=None,
|
||||
) == ("https://smith.langchain.com/studio/?baseUrl=http%3A%2F%2F127.0.0.1%3A8123")
|
||||
|
||||
|
||||
def test_studio_link_supports_self_hosted_and_remote_urls() -> None:
|
||||
assert _studio_link(
|
||||
port=8123,
|
||||
studio_url="https://langsmith.example.com/prefix/",
|
||||
api_url="https://api.example.com/graph?tenant=a®ion=eu",
|
||||
debugger_base_url=None,
|
||||
) == (
|
||||
"https://langsmith.example.com/prefix/studio/"
|
||||
"?baseUrl=https%3A%2F%2Fapi.example.com%2Fgraph%3Ftenant%3Da%26region%3Deu"
|
||||
)
|
||||
|
||||
|
||||
def test_studio_link_supports_deprecated_debugger_base_url(capsys) -> None:
|
||||
assert _studio_link(
|
||||
port=8123,
|
||||
studio_url=None,
|
||||
api_url=None,
|
||||
debugger_base_url="https://api.example.com",
|
||||
).endswith("?baseUrl=https%3A%2F%2Fapi.example.com")
|
||||
assert "--debugger-base-url is deprecated; use --api-url" in capsys.readouterr().err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("studio_url", "api_url"),
|
||||
[
|
||||
("javascript:alert(1)", None),
|
||||
("https://user:password@example.com", None),
|
||||
("https://smith.langchain.com?workspace=test", None),
|
||||
(None, "file:///tmp/langgraph.sock"),
|
||||
(None, "https://user:password@example.com"),
|
||||
],
|
||||
)
|
||||
def test_studio_link_rejects_unsafe_urls(
|
||||
studio_url: str | None, api_url: str | None
|
||||
) -> None:
|
||||
with pytest.raises(click.UsageError):
|
||||
_studio_link(
|
||||
port=8123,
|
||||
studio_url=studio_url,
|
||||
api_url=api_url,
|
||||
debugger_base_url=None,
|
||||
)
|
||||
|
||||
|
||||
def test_studio_link_rejects_conflicting_api_url_aliases() -> None:
|
||||
with pytest.raises(
|
||||
click.UsageError,
|
||||
match="cannot specify different URLs",
|
||||
):
|
||||
_studio_link(
|
||||
port=8123,
|
||||
studio_url=None,
|
||||
api_url="https://api.example.com",
|
||||
debugger_base_url="https://other.example.com",
|
||||
)
|
||||
|
||||
|
||||
def test_top_level_help_shows_deploy_subcommands() -> None:
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
@@ -34,30 +34,6 @@ def invoke(monkeypatch, args, handler, input=None):
|
||||
@pytest.mark.parametrize("mode", [None, "--summary", "--raw"])
|
||||
def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
|
||||
charts = deployment_metrics_response
|
||||
server_summary = {
|
||||
"deployment_id": DEPLOYMENT_ID,
|
||||
"step_seconds": 60,
|
||||
"sections": {
|
||||
"API": {
|
||||
"Metrics": {
|
||||
metric: {
|
||||
"metric": metric,
|
||||
"unit": unit,
|
||||
"latest": 1.234567,
|
||||
"min": 1.234567,
|
||||
"max": 1.234567,
|
||||
"avg": 1.234567,
|
||||
}
|
||||
for metric, unit in (
|
||||
("cpu_usage", "percent"),
|
||||
("replica_count", "count"),
|
||||
("p95_latency", "milliseconds"),
|
||||
("responses_per_second", "responses_per_second"),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
@@ -81,8 +57,6 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
|
||||
"end_time": "2026-10-28T23:02:33+00:00",
|
||||
**options,
|
||||
}
|
||||
if mode == "--summary":
|
||||
params["summary"] = "true"
|
||||
|
||||
def handler(request):
|
||||
assert request.method == "GET"
|
||||
@@ -90,9 +64,7 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
|
||||
assert dict(request.url.params) == params
|
||||
assert request.headers["x-tenant-id"] == "workspace-id"
|
||||
assert request.headers["x-api-key"] == "key"
|
||||
return httpx.Response(
|
||||
200, json=server_summary if mode == "--summary" else charts
|
||||
)
|
||||
return httpx.Response(200, json=charts)
|
||||
|
||||
args = ["metrics", DEPLOYMENT_ID]
|
||||
for key, value in options.items():
|
||||
@@ -101,19 +73,19 @@ def test_metrics(monkeypatch, bounds, mode, deployment_metrics_response):
|
||||
args.append(mode)
|
||||
result = invoke(monkeypatch, args, handler)
|
||||
assert result.exit_code == 0, result.output
|
||||
if mode == "--summary":
|
||||
expected = server_summary
|
||||
elif mode == "--raw":
|
||||
expected = charts
|
||||
else:
|
||||
expected = compact_deployment_metrics(
|
||||
expected = (
|
||||
charts
|
||||
if mode == "--raw"
|
||||
else compact_deployment_metrics(
|
||||
charts,
|
||||
DEPLOYMENT_ID,
|
||||
start_time=params["start_time"],
|
||||
end_time=params["end_time"],
|
||||
start_time=params.get("start_time"),
|
||||
end_time=params.get("end_time"),
|
||||
summary=mode == "--summary",
|
||||
)
|
||||
)
|
||||
assert json.loads(result.output) == expected
|
||||
if mode is None:
|
||||
if mode != "--raw":
|
||||
assert expected["start_time"] == params["start_time"]
|
||||
assert expected["end_time"] == params["end_time"]
|
||||
assert result.output.strip() == json.dumps(expected, separators=(",", ":"))
|
||||
|
||||
@@ -53,15 +53,22 @@ def test_compact_metrics(deployment_metrics_response):
|
||||
assert result["sections"]["API"]["Latency"]["P95 Latency"]["unit"] is None
|
||||
|
||||
|
||||
def test_requested_bounds(deployment_metrics_response):
|
||||
def test_summary_and_requested_bounds(deployment_metrics_response):
|
||||
result = reshape(
|
||||
deployment_metrics_response,
|
||||
"id",
|
||||
start_time="2026-10-27T23:00:00Z",
|
||||
end_time="2026-10-28T01:00:00Z",
|
||||
summary=True,
|
||||
)
|
||||
assert result["start_time"] == "2026-10-27T23:00:00Z"
|
||||
assert result["end_time"] == "2026-10-28T01:00:00Z"
|
||||
for section in result["sections"].values():
|
||||
if section == {"_empty": True}:
|
||||
continue
|
||||
for chart in section.values():
|
||||
for series in chart.values():
|
||||
assert set(series) == {"metric", "unit", "latest", "min", "max", "avg"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("values", [[], [None], [float("nan"), float("inf")]])
|
||||
|
||||
@@ -16,7 +16,7 @@ DEFAULT_DOCKER_CAPABILITIES = DockerCapabilities(
|
||||
)
|
||||
|
||||
|
||||
def test_compose_with_no_debugger_and_custom_db():
|
||||
def test_compose_with_custom_db():
|
||||
port = 8123
|
||||
custom_postgres_uri = "custom_postgres_uri"
|
||||
actual_compose_str = compose(
|
||||
@@ -42,7 +42,7 @@ def test_compose_with_no_debugger_and_custom_db():
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
|
||||
def test_compose_with_custom_db_and_healthcheck():
|
||||
port = 8123
|
||||
custom_postgres_uri = "custom_postgres_uri"
|
||||
actual_compose_str = compose(
|
||||
@@ -71,39 +71,11 @@ def test_compose_with_no_debugger_and_custom_db_with_healthcheck():
|
||||
test: python /api/healthcheck.py
|
||||
interval: 60s
|
||||
start_interval: 1s
|
||||
start_period: 10s"""
|
||||
start_period: 60s"""
|
||||
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-redis:
|
||||
image: redis:6
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}:8000"
|
||||
depends_on:
|
||||
langgraph-redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
REDIS_URI: redis://langgraph-redis:6379
|
||||
POSTGRES_URI: {custom_postgres_uri}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_debugger_and_default_db():
|
||||
def test_compose_with_default_db():
|
||||
port = 8123
|
||||
actual_compose_str = compose(DEFAULT_DOCKER_CAPABILITIES, port=port)
|
||||
expected_compose_str = f"""volumes:
|
||||
@@ -302,72 +274,6 @@ def test_compose_with_api_version_and_custom_postgres():
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_with_api_version_and_debugger():
|
||||
"""Test compose function with api_version and debugger port."""
|
||||
port = 8123
|
||||
debugger_port = 8001
|
||||
api_version = "0.2.74"
|
||||
|
||||
actual_compose_str = compose(
|
||||
DEFAULT_DOCKER_CAPABILITIES,
|
||||
port=port,
|
||||
api_version=api_version,
|
||||
debugger_port=debugger_port,
|
||||
)
|
||||
|
||||
expected_compose_str = 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: pgvector/pgvector:pg16
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- shared_preload_libraries=vector
|
||||
volumes:
|
||||
- langgraph-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready -U postgres
|
||||
start_period: 10s
|
||||
timeout: 1s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
langgraph-debugger:
|
||||
image: langchain/langgraph-debugger
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
langgraph-postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "{debugger_port}:3968"
|
||||
langgraph-api:
|
||||
ports:
|
||||
- "{port}: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}"""
|
||||
assert clean_empty_lines(actual_compose_str) == expected_compose_str
|
||||
|
||||
|
||||
def test_compose_distributed_mode_with_custom_db():
|
||||
"""Test compose with engine_runtime_mode='distributed' adds N_JOBS_PER_WORKER=0."""
|
||||
port = 8123
|
||||
|
||||
Reference in new issue
Block a user