mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-23 08:21:49 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba7b9ff89 |
@@ -23,7 +23,6 @@ from langgraph_cli.schemas import (
|
||||
GraphDef,
|
||||
HttpConfig,
|
||||
IndexConfig,
|
||||
PriorityConfig,
|
||||
SecurityConfig,
|
||||
SerdeConfig,
|
||||
StoreConfig,
|
||||
@@ -114,7 +113,6 @@ def add_descriptions_to_schema(schema, cls):
|
||||
StoreConfig,
|
||||
IndexConfig,
|
||||
AuthConfig,
|
||||
PriorityConfig,
|
||||
SecurityConfig,
|
||||
HttpConfig,
|
||||
CorsConfig,
|
||||
|
||||
@@ -847,7 +847,6 @@ def dev(
|
||||
store=config_json.get("store"),
|
||||
wait_for_client=wait_for_client,
|
||||
auth=config_json.get("auth"),
|
||||
priority=config_json.get("priority"),
|
||||
http=config_json.get("http"),
|
||||
ui=config_json.get("ui"),
|
||||
ui_config=config_json.get("ui_config"),
|
||||
|
||||
@@ -442,7 +442,6 @@ def validate_config(
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"encryption": config.get("encryption"),
|
||||
"priority": config.get("priority"),
|
||||
"http": config.get("http"),
|
||||
# Pass through webhooks config so it can be injected into the image
|
||||
"webhooks": config.get("webhooks"),
|
||||
@@ -607,19 +606,6 @@ def validate_config(
|
||||
f"Invalid encryption.path format: '{encryption_conf['path']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
# Validate priority config
|
||||
if priority_conf := config.get("priority"):
|
||||
if "path" in priority_conf:
|
||||
if ":" not in priority_conf["path"]:
|
||||
raise ValueError(
|
||||
f"Invalid priority.path format: '{priority_conf['path']}'. "
|
||||
"Must be in format './path/to/file.py:attribute_name'"
|
||||
)
|
||||
if _is_node_graph(priority_conf["path"]):
|
||||
raise ValueError(
|
||||
"JavaScript priority callbacks are not supported: "
|
||||
f"'{priority_conf['path']}'. Use a Python file or module instead."
|
||||
)
|
||||
if http_conf := config.get("http"):
|
||||
if "app" in http_conf:
|
||||
if ":" not in http_conf["app"]:
|
||||
@@ -663,7 +649,6 @@ _KNOWN_CONFIG_KEYS = {
|
||||
"store",
|
||||
"auth",
|
||||
"encryption",
|
||||
"priority",
|
||||
"http",
|
||||
"webhooks",
|
||||
"checkpointer",
|
||||
@@ -1095,49 +1080,6 @@ def _update_encryption_path(
|
||||
)
|
||||
|
||||
|
||||
def _update_priority_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
"""Update priority.path to use Docker container paths."""
|
||||
priority_conf = config.get("priority")
|
||||
if not priority_conf or not (path_str := priority_conf.get("path")):
|
||||
return
|
||||
|
||||
module_str, sep, attr_str = path_str.partition(":")
|
||||
if not sep or not module_str.startswith("."):
|
||||
return # Already validated or absolute path
|
||||
|
||||
resolved = config_path.parent / module_str
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Priority file not found: {resolved} (from {path_str})"
|
||||
)
|
||||
if not resolved.is_file():
|
||||
raise IsADirectoryError(f"Priority path must be a file: {resolved}")
|
||||
|
||||
# Check faux packages first (they take precedence)
|
||||
for faux_path, (_, destpath) in local_deps.faux_pkgs.items():
|
||||
if resolved.is_relative_to(faux_path):
|
||||
new_path = f"{destpath}/{resolved.relative_to(faux_path)}:{attr_str}"
|
||||
priority_conf["path"] = new_path
|
||||
return
|
||||
|
||||
# Check real packages
|
||||
for real_path in local_deps.real_pkgs:
|
||||
if resolved.is_relative_to(real_path):
|
||||
new_path = (
|
||||
f"/deps/{real_path.name}/{resolved.relative_to(real_path)}:{attr_str}"
|
||||
)
|
||||
priority_conf["path"] = new_path
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
f"Priority file '{resolved}' not covered by dependencies.\n"
|
||||
"Add its parent directory to the 'dependencies' array in your config.\n"
|
||||
f"Current dependencies: {config['dependencies']}"
|
||||
)
|
||||
|
||||
|
||||
def _update_checkpointer_path(
|
||||
config_path: pathlib.Path, config: Config, local_deps: LocalDeps
|
||||
) -> None:
|
||||
@@ -1271,9 +1213,6 @@ def _build_runtime_env_vars(config: Config) -> list[str]:
|
||||
if (encryption_config := config.get("encryption")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_ENCRYPTION='{json.dumps(encryption_config)}'")
|
||||
|
||||
if (priority_config := config.get("priority")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_PRIORITY='{json.dumps(priority_config)}'")
|
||||
|
||||
if (http_config := config.get("http")) is not None:
|
||||
env_vars.append(f"ENV LANGGRAPH_HTTP='{json.dumps(http_config)}'")
|
||||
|
||||
@@ -1441,8 +1380,6 @@ def python_config_to_docker(
|
||||
_update_auth_path(config_path, config, local_deps)
|
||||
# Rewrite encryption path, so it points to the correct location in the Docker container
|
||||
_update_encryption_path(config_path, config, local_deps)
|
||||
# Rewrite priority path, so it points to the correct location in the Docker container
|
||||
_update_priority_path(config_path, config, local_deps)
|
||||
# Rewrite checkpointer path, so it points to the correct location in the Docker container
|
||||
_update_checkpointer_path(config_path, config, local_deps)
|
||||
# Rewrite HTTP app path, so it points to the correct location in the Docker container
|
||||
|
||||
@@ -1106,12 +1106,24 @@ class _ProgressReader:
|
||||
return self._file_size
|
||||
|
||||
|
||||
def _upload_to_gcs(signed_url: str, file_path: str, file_size: int) -> None:
|
||||
"""Upload tarball to GCS via signed PUT URL with progress display."""
|
||||
def _upload_to_gcs(
|
||||
signed_url: str,
|
||||
file_path: str,
|
||||
file_size: int,
|
||||
upload_headers: dict[str, str] | None,
|
||||
) -> None:
|
||||
"""Upload tarball via signed PUT URL with progress display."""
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
em = _get_emitter()
|
||||
# Control planes that return no `upload_headers` sign for the Cloud Storage
|
||||
# size range. Content-Type is a fallback: without one, urllib sends a form
|
||||
# content type, which breaks signatures that cover it.
|
||||
headers = {
|
||||
"Content-Type": "application/gzip",
|
||||
**(upload_headers or {"X-Goog-Content-Length-Range": "0,209715200"}),
|
||||
}
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
reader = _ProgressReader(f, file_size, em)
|
||||
@@ -1119,11 +1131,7 @@ def _upload_to_gcs(signed_url: str, file_path: str, file_size: int) -> None:
|
||||
signed_url,
|
||||
data=reader,
|
||||
method="PUT",
|
||||
headers={
|
||||
"Content-Type": "application/gzip",
|
||||
"Content-Length": str(file_size),
|
||||
"X-Goog-Content-Length-Range": "0,209715200",
|
||||
},
|
||||
headers={**headers, "Content-Length": str(file_size)},
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=_UPLOAD_TIMEOUT_SECONDS)
|
||||
@@ -1443,7 +1451,9 @@ def _run_remote_build(
|
||||
step += 1
|
||||
|
||||
_log_deploy_step(step, "Uploading source")
|
||||
_upload_to_gcs(signed_url, archive_path, file_size)
|
||||
_upload_to_gcs(
|
||||
signed_url, archive_path, file_size, upload_data.get("upload_headers")
|
||||
)
|
||||
step += 1
|
||||
|
||||
_log_deploy_step(step, "Triggering remote build")
|
||||
|
||||
@@ -258,10 +258,13 @@ class HostBackendClient:
|
||||
)
|
||||
|
||||
def request_upload_url(self, deployment_id: str) -> dict[str, Any]:
|
||||
"""Get a signed GCS URL for uploading the source tarball."""
|
||||
"""Get a signed URL for uploading the source tarball."""
|
||||
# Tells the control plane this client sends the returned
|
||||
# `upload_headers` with its upload, which some storage requires.
|
||||
return self._request(
|
||||
"POST",
|
||||
f"/v2/deployments/{deployment_id}/upload-url",
|
||||
{"supports_upload_headers": True},
|
||||
)
|
||||
|
||||
def update_deployment(
|
||||
|
||||
@@ -373,27 +373,6 @@ class EncryptionConfig(TypedDict, total=False):
|
||||
"""
|
||||
|
||||
|
||||
class PriorityConfig(TypedDict, total=False):
|
||||
"""Configuration for custom run priority logic.
|
||||
|
||||
Allows you to decide each thread's priority settings with a Python function
|
||||
that the server calls at the first run on a thread with no settings, which
|
||||
can come long after the thread was created."""
|
||||
|
||||
path: str
|
||||
"""Required. Path to an instance of the Priority() class that registers an assign function.
|
||||
|
||||
Format: "path/to/file.py:my_priority"
|
||||
|
||||
Example:
|
||||
{
|
||||
"priority": {
|
||||
"path": "./priority.py:my_priority"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class CorsConfig(TypedDict, total=False):
|
||||
"""Specifies Cross-Origin Resource Sharing (CORS) rules for your server.
|
||||
|
||||
@@ -774,13 +753,6 @@ class Config(TypedDict, total=False):
|
||||
Allows you to implement custom encryption for sensitive data stored in the database.
|
||||
"""
|
||||
|
||||
priority: PriorityConfig | None
|
||||
"""Optional. Custom run priority config, including the path to your Python priority logic.
|
||||
|
||||
Allows you to decide each thread's priority settings at the first run on a thread with no settings,
|
||||
which can come long after the thread was created.
|
||||
"""
|
||||
|
||||
http: HttpConfig | None
|
||||
"""Optional. Configuration for the built-in HTTP server, controlling which custom routes are exposed
|
||||
and how cross-origin requests are handled.
|
||||
@@ -812,7 +784,6 @@ __all__ = [
|
||||
"CheckpointerConfig",
|
||||
"AuthConfig",
|
||||
"EncryptionConfig",
|
||||
"PriorityConfig",
|
||||
"HttpConfig",
|
||||
"MiddlewareOrders",
|
||||
"Distros",
|
||||
|
||||
@@ -910,7 +910,6 @@ def python_config_to_docker_uv_lock(
|
||||
for section, key in [
|
||||
("auth", "path"),
|
||||
("encryption", "path"),
|
||||
("priority", "path"),
|
||||
("checkpointer", "path"),
|
||||
("http", "app"),
|
||||
]:
|
||||
|
||||
@@ -196,17 +196,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -449,17 +438,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/$defs/UvSource"
|
||||
},
|
||||
@@ -688,17 +666,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1203,18 +1170,6 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"PriorityConfig": {
|
||||
"title": "PriorityConfig",
|
||||
"description": "Configuration for custom run priority logic.\n\nAllows you to decide each thread's priority settings with a Python function\nthat the server calls at the first run on a thread with no settings, which\ncan come long after the thread was created.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Path to an instance of the Priority() class that registers an assign function.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"UvSource": {
|
||||
"title": "UvSource",
|
||||
"description": "Deployment source rooted at a uv project or workspace.",
|
||||
|
||||
@@ -196,17 +196,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -449,17 +438,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/$defs/UvSource"
|
||||
},
|
||||
@@ -688,17 +666,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/$defs/PriorityConfig"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Custom run priority config, including the path to your Python priority logic.\n\nAllows you to decide each thread's priority settings at the first run on a thread with no settings,\nwhich can come long after the thread was created.\n"
|
||||
},
|
||||
"source": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1203,18 +1170,6 @@
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"PriorityConfig": {
|
||||
"title": "PriorityConfig",
|
||||
"description": "Configuration for custom run priority logic.\n\nAllows you to decide each thread's priority settings with a Python function\nthat the server calls at the first run on a thread with no settings, which\ncan come long after the thread was created.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Required. Path to an instance of the Priority() class that registers an assign function.\n"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"UvSource": {
|
||||
"title": "UvSource",
|
||||
"description": "Deployment source rooted at a uv project or workspace.",
|
||||
|
||||
@@ -2,12 +2,10 @@ import json
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import click
|
||||
from click.testing import CliRunner
|
||||
@@ -373,32 +371,6 @@ def test_dev_command_requires_ssl_certfile_and_keyfile_together(tmp_path) -> Non
|
||||
assert "Both --ssl-certfile and --ssl-keyfile must be provided" in result.output
|
||||
|
||||
|
||||
def test_dev_command_passes_priority_to_the_server(tmp_path, monkeypatch) -> None:
|
||||
config_path = tmp_path / "langgraph.json"
|
||||
config_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"priority": {"path": "./priority.py:priority"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
# The test environment doesn't install langgraph-api, so stand in for its server.
|
||||
server = MagicMock()
|
||||
monkeypatch.setitem(sys.modules, "langgraph_api.cli", server)
|
||||
# dev appends the working directory and its dependencies to sys.path.
|
||||
monkeypatch.setattr(sys, "path", list(sys.path))
|
||||
|
||||
result = CliRunner().invoke(cli, ["dev", "--config", str(config_path)])
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
assert server.run_server.call_args.kwargs["priority"] == {
|
||||
"path": "./priority.py:priority"
|
||||
}
|
||||
|
||||
|
||||
def test_deploy_list_command(monkeypatch) -> None:
|
||||
runner = CliRunner()
|
||||
captured: dict[str, str] = {}
|
||||
|
||||
@@ -96,6 +96,7 @@ class ControlPlaneDouble:
|
||||
existing_deployments: list[dict] = field(default_factory=list)
|
||||
push_token_status: int = 200
|
||||
push_token_extra: dict = field(default_factory=dict)
|
||||
upload_url_extra: dict = field(default_factory=dict)
|
||||
create_error: str | None = None
|
||||
listeners: list[dict] = field(default_factory=list)
|
||||
listeners_by_id: dict[str, dict] = field(default_factory=dict)
|
||||
@@ -159,7 +160,12 @@ class ControlPlaneDouble:
|
||||
)
|
||||
if path.endswith("/upload-url"):
|
||||
return httpx.Response(
|
||||
200, json={"upload_url": SIGNED_UPLOAD_URL, "object_path": OBJECT_PATH}
|
||||
200,
|
||||
json={
|
||||
"upload_url": SIGNED_UPLOAD_URL,
|
||||
"object_path": OBJECT_PATH,
|
||||
**self.upload_url_extra,
|
||||
},
|
||||
)
|
||||
if method == "PATCH":
|
||||
return httpx.Response(200, json={"tenant_id": "tenant-1"})
|
||||
@@ -276,7 +282,7 @@ class DeployProject:
|
||||
control_plane: ControlPlaneDouble
|
||||
docker: DockerDouble
|
||||
timeline: list[str]
|
||||
uploads: list[tuple[str, str, int]]
|
||||
uploads: list[tuple[str, str, int, dict[str, str] | None]]
|
||||
|
||||
def run(self, *args: str, host_url: str = CONTROL_PLANE_URL) -> Result:
|
||||
return CliRunner().invoke(
|
||||
@@ -313,16 +319,21 @@ def deploy_project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> DeployPro
|
||||
timeline: list[str] = []
|
||||
control_plane = ControlPlaneDouble(timeline)
|
||||
docker = DockerDouble(timeline)
|
||||
uploads: list[tuple[str, str, int]] = []
|
||||
uploads: list[tuple[str, str, int, dict[str, str] | None]] = []
|
||||
|
||||
@contextmanager
|
||||
def fake_create_archive(config_path: Path, config: dict) -> Iterator[tuple]:
|
||||
timeline.append("create_archive")
|
||||
yield ARCHIVE
|
||||
|
||||
def fake_upload(signed_url: str, file_path: str, file_size: int) -> None:
|
||||
def fake_upload(
|
||||
signed_url: str,
|
||||
file_path: str,
|
||||
file_size: int,
|
||||
upload_headers: dict[str, str] | None,
|
||||
) -> None:
|
||||
timeline.append("upload_archive")
|
||||
uploads.append((signed_url, file_path, file_size))
|
||||
uploads.append((signed_url, file_path, file_size, upload_headers))
|
||||
|
||||
monkeypatch.setattr(deploy_module, "_no_input", False)
|
||||
monkeypatch.setattr(deploy_module, "_emitter", None)
|
||||
@@ -699,6 +710,9 @@ def test_deployment_not_created_by_the_cli_gets_an_actionable_error(
|
||||
def test_remote_build_creates_an_internal_source_deployment_and_uploads_the_archive(
|
||||
deploy_project: DeployProject,
|
||||
) -> None:
|
||||
upload_headers = {"Content-Type": "application/gzip", "x-ms-blob-type": "BlockBlob"}
|
||||
deploy_project.control_plane.upload_url_extra = {"upload_headers": upload_headers}
|
||||
|
||||
result = deploy_project.run("--remote", "--install-command", "yarn install")
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
@@ -713,7 +727,12 @@ def test_remote_build_creates_an_internal_source_deployment_and_uploads_the_arch
|
||||
assert deploy_project.control_plane.bodies[CREATE_DEPLOYMENT]["source"] == (
|
||||
"internal_source"
|
||||
)
|
||||
assert deploy_project.uploads == [(SIGNED_UPLOAD_URL, ARCHIVE[0], ARCHIVE[1])]
|
||||
assert deploy_project.control_plane.bodies[_upload_url(CREATED_ID)] == {
|
||||
"supports_upload_headers": True
|
||||
}
|
||||
assert deploy_project.uploads == [
|
||||
(SIGNED_UPLOAD_URL, ARCHIVE[0], ARCHIVE[1], upload_headers)
|
||||
]
|
||||
assert deploy_project.control_plane.bodies[_patch(CREATED_ID)] == {
|
||||
"revision_source": "internal_source",
|
||||
"source_revision_config": {
|
||||
|
||||
@@ -16,7 +16,6 @@ from langgraph_cli.config import (
|
||||
config_to_docker,
|
||||
default_base_image,
|
||||
docker_tag,
|
||||
get_unknown_keys,
|
||||
has_disallowed_build_command_content,
|
||||
validate_config,
|
||||
validate_config_file,
|
||||
@@ -156,7 +155,6 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"encryption": None,
|
||||
"priority": None,
|
||||
"webhooks": None,
|
||||
"checkpointer": None,
|
||||
"http": None,
|
||||
@@ -186,7 +184,6 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"encryption": None,
|
||||
"priority": None,
|
||||
"webhooks": None,
|
||||
"checkpointer": None,
|
||||
"http": None,
|
||||
@@ -1292,109 +1289,6 @@ def test_config_to_docker_python_encryption_formatted():
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path", ["./priority.py:priority", "my_package.priority:priority"]
|
||||
)
|
||||
def test_config_to_docker_python_priority(path: str):
|
||||
# Test that priority config is kept by validation and isn't an unknown key
|
||||
raw_config = {
|
||||
"python_version": "3.11",
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"priority": {"path": path},
|
||||
}
|
||||
validated = validate_config(raw_config)
|
||||
|
||||
assert validated["priority"] == {"path": path}
|
||||
assert get_unknown_keys(raw_config) == []
|
||||
|
||||
|
||||
def test_config_to_docker_python_priority_bad_path():
|
||||
# Test that invalid priority path format raises ValueError
|
||||
with pytest.raises(ValueError, match="Invalid priority.path format"):
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"priority": {"path": "./priority.py"}, # Missing :attribute
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ext", [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"])
|
||||
def test_config_to_docker_python_priority_js_path(ext: str):
|
||||
# Test that a JavaScript priority path is rejected
|
||||
with pytest.raises(ValueError, match="JavaScript priority callbacks"):
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"graphs": {"agent": "./agent.py:graph"},
|
||||
"dependencies": ["."],
|
||||
"priority": {"path": f"./priority{ext}:priority"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_nodejs_priority():
|
||||
# Test that a Node.js-only project can name a Python priority file
|
||||
actual_docker_stdin, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"node_version": "20",
|
||||
"graphs": {"agent": "./graphs/agent.js:graph"},
|
||||
"priority": {"path": "./priority.py:priority"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraphjs-api",
|
||||
)
|
||||
assert (
|
||||
"""ENV LANGGRAPH_PRIORITY='{"path": "./priority.py:priority"}'"""
|
||||
in actual_docker_stdin
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_python_priority_formatted():
|
||||
# Test that priority config is written to the Docker output with its container path
|
||||
actual_docker_stdin, _ = config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["."],
|
||||
"graphs": {"agent": "./graphs/agent.py:graph"},
|
||||
"priority": {"path": "./agent.py:my_priority"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
assert (
|
||||
"ENV LANGGRAPH_PRIORITY="
|
||||
"""'{"path": "/deps/outer-unit_tests/unit_tests/agent.py:my_priority"}'"""
|
||||
in actual_docker_stdin
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_python_priority_not_in_dependencies():
|
||||
# Test that a priority file outside the dependencies raises ValueError
|
||||
with pytest.raises(
|
||||
ValueError, match="Priority file .* not covered by dependencies"
|
||||
):
|
||||
config_to_docker(
|
||||
PATH_TO_CONFIG,
|
||||
validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"dependencies": ["./graphs"],
|
||||
"graphs": {"agent": "./graphs/agent.py:graph"},
|
||||
"priority": {"path": "./agent.py:my_priority"},
|
||||
}
|
||||
),
|
||||
base_image="langchain/langgraph-api",
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_nodejs_internal_docker_tag():
|
||||
graphs = {"agent": "./graphs/agent.js:graph"}
|
||||
actual_docker_stdin, additional_contexts = config_to_docker(
|
||||
@@ -1837,38 +1731,6 @@ def test_config_to_docker_uv_lock_honors_root_workspace_sources():
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_uv_lock_rewrites_priority_path():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = pathlib.Path(tmpdir)
|
||||
project_root, config_path = _write_uv_lock_workspace(
|
||||
tmpdir_path,
|
||||
agent_sources="[tool.uv.sources]\nshared = { workspace = true }",
|
||||
)
|
||||
(project_root / "libs" / "shared" / "src" / "shared" / "priority.py").touch()
|
||||
|
||||
config = validate_config(
|
||||
{
|
||||
"python_version": "3.11",
|
||||
"graphs": {
|
||||
"agent": "../../apps/agent/src/agent/graph.py:graph",
|
||||
},
|
||||
"source": {"kind": "uv", "root": "../..", "package": "agent"},
|
||||
"priority": {
|
||||
"path": "../../libs/shared/src/shared/priority.py:priority"
|
||||
},
|
||||
}
|
||||
)
|
||||
docker, _ = config_to_docker(
|
||||
config_path, config, base_image="langchain/langgraph-api:0.2.47"
|
||||
)
|
||||
|
||||
assert (
|
||||
"ENV LANGGRAPH_PRIORITY="
|
||||
"""'{"path": "/deps/workspace/libs/shared/src/shared/priority.py:priority"}'"""
|
||||
in docker
|
||||
)
|
||||
|
||||
|
||||
def test_config_to_docker_uv_lock_ignores_unrelated_workspace_package_sources():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmpdir_path = pathlib.Path(tmpdir)
|
||||
|
||||
@@ -1414,3 +1414,59 @@ class TestPlacementOnAKnownListener:
|
||||
def test_refuses_and_names_the_namespaces(self, request_, listener, message):
|
||||
with pytest.raises(click.UsageError, match=message):
|
||||
request_.on(listener)
|
||||
|
||||
|
||||
DEFAULT_UPLOAD_HEADERS = {
|
||||
"content-type": "application/gzip",
|
||||
"content-length": "7",
|
||||
"x-goog-content-length-range": "0,209715200",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("upload_headers", "expected"),
|
||||
[
|
||||
pytest.param(None, DEFAULT_UPLOAD_HEADERS, id="missing_sends_defaults"),
|
||||
pytest.param({}, DEFAULT_UPLOAD_HEADERS, id="empty_sends_defaults"),
|
||||
pytest.param(
|
||||
{
|
||||
"content-type": "application/x-gzip",
|
||||
"x-ms-blob-type": "BlockBlob",
|
||||
"content-length": "1",
|
||||
},
|
||||
{
|
||||
"content-type": "application/x-gzip",
|
||||
"x-ms-blob-type": "BlockBlob",
|
||||
"content-length": "7",
|
||||
},
|
||||
id="returned_headers_replace_defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{"x-ms-blob-type": "BlockBlob"},
|
||||
{
|
||||
"content-type": "application/gzip",
|
||||
"x-ms-blob-type": "BlockBlob",
|
||||
"content-length": "7",
|
||||
},
|
||||
id="returned_headers_keep_a_content_type",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_source_upload_sends_the_headers_the_signed_url_requires(
|
||||
tmp_path, mocker, upload_headers, expected
|
||||
):
|
||||
archive = tmp_path / "source.tar.gz"
|
||||
archive.write_bytes(b"tarball")
|
||||
sent = []
|
||||
mocker.patch(
|
||||
"langgraph_cli.deploy._get_emitter", return_value=MagicMock(json_mode=True)
|
||||
)
|
||||
mocker.patch(
|
||||
"urllib.request.urlopen", side_effect=lambda req, timeout: sent.append(req)
|
||||
)
|
||||
|
||||
deploy_mod._upload_to_gcs(
|
||||
"https://storage.example/signed", str(archive), 7, upload_headers
|
||||
)
|
||||
|
||||
assert {name.lower(): value for name, value in sent[0].header_items()} == expected
|
||||
|
||||
Generated
+1
-1
@@ -1919,7 +1919,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14" },
|
||||
{ name = "websockets", specifier = ">=14,<17" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
Generated
+1
-1
@@ -614,7 +614,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14" },
|
||||
{ name = "websockets", specifier = ">=14,<17" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
@@ -34,14 +34,6 @@
|
||||
- **`thread.output`** — awaitable that resolves to the terminal thread state
|
||||
`values` dict after the run lifecycle completes.
|
||||
|
||||
- **`Priority` (beta)** — register an async function with `@priority.assign`
|
||||
and name the object in the `priority` section of `langgraph.json`. The
|
||||
server calls the function with a `PriorityContext` at the first run on a
|
||||
thread with no settings and stores the `PrioritySettings` it returns
|
||||
(`priority`, `fairness_group`, `fairness_weight`) on the thread. Requires
|
||||
`langgraph-api` 0.18.0 or later and a `langgraph-cli` that supports the
|
||||
section; older versions of either ignore it.
|
||||
|
||||
### Changed
|
||||
|
||||
- `client.threads.stream()` now accepts `transport="sse"` (default) or
|
||||
|
||||
@@ -2,8 +2,6 @@ from langgraph_sdk.auth import Auth
|
||||
from langgraph_sdk.client import get_client, get_sync_client
|
||||
from langgraph_sdk.encryption import Encryption
|
||||
from langgraph_sdk.encryption.types import DecryptResult, EncryptionContext
|
||||
from langgraph_sdk.priority import Priority
|
||||
from langgraph_sdk.priority.types import PriorityContext, PrioritySettings
|
||||
|
||||
__version__ = "0.4.8"
|
||||
|
||||
@@ -12,9 +10,6 @@ __all__ = [
|
||||
"DecryptResult",
|
||||
"Encryption",
|
||||
"EncryptionContext",
|
||||
"Priority",
|
||||
"PriorityContext",
|
||||
"PrioritySettings",
|
||||
"get_client",
|
||||
"get_sync_client",
|
||||
]
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
"""Custom run priority for LangGraph.
|
||||
|
||||
.. warning::
|
||||
This API is in beta and may change in future versions.
|
||||
|
||||
This module lets developers decide the priority settings of each thread with
|
||||
an async function that the server calls at the first run on a thread with no
|
||||
settings. The function is registered on an object that `langgraph.json` names,
|
||||
and runs server-side.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import typing
|
||||
import warnings
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from langgraph_sdk.encryption import DuplicateHandlerError, LangGraphBetaWarning
|
||||
from langgraph_sdk.priority import types
|
||||
|
||||
_AssignHandler = Callable[
|
||||
[types.PriorityContext], Awaitable[types.PrioritySettings | None]
|
||||
]
|
||||
_AssignHandlerT = typing.TypeVar("_AssignHandlerT", bound=_AssignHandler)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _warn_priority_beta() -> None:
|
||||
warnings.warn(
|
||||
"The Priority API is in beta and may change in future versions.",
|
||||
LangGraphBetaWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
|
||||
|
||||
def _validate_handler(fn: typing.Callable) -> None:
|
||||
"""Validate that the assign handler is async and takes one parameter.
|
||||
|
||||
Raises:
|
||||
TypeError: If the handler is not an async function or has wrong parameter count
|
||||
"""
|
||||
if not inspect.iscoroutinefunction(fn):
|
||||
raise TypeError(
|
||||
f"Priority assign handler must be an async function, got {type(fn)}"
|
||||
)
|
||||
|
||||
sig = inspect.signature(fn)
|
||||
params = [
|
||||
p
|
||||
for p in sig.parameters.values()
|
||||
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
||||
]
|
||||
if len(params) != 1:
|
||||
raise TypeError(
|
||||
"Priority assign handler must accept exactly 1 parameter (ctx), "
|
||||
f"got {len(params)}"
|
||||
)
|
||||
|
||||
|
||||
class Priority:
|
||||
"""Decide the priority settings of your LangGraph application's threads.
|
||||
|
||||
.. warning::
|
||||
This API is in beta and may change in future versions.
|
||||
|
||||
To use, create a separate Python file and add the path to the file to your
|
||||
LangGraph API configuration file (`langgraph.json`). Within that file, create
|
||||
an instance of the Priority class and register an assign function with
|
||||
`@priority.assign`.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./my_agent/agent.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"priority": {
|
||||
"path": "./priority.py:priority"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The server calls the assign function at the first run on a thread with no
|
||||
settings, which can come long after the thread was created, and stores the
|
||||
settings it returns on the thread. Later runs on the thread use the stored
|
||||
settings. The function can be called more than once for one thread, for
|
||||
example after it raises, and only one result is stored, so keep it free of
|
||||
side effects.
|
||||
|
||||
???+ example "Basic Usage"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Priority, PriorityContext, PrioritySettings
|
||||
|
||||
priority = Priority()
|
||||
|
||||
@priority.assign
|
||||
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return {
|
||||
"priority": 3 if "premium" in ctx.permissions else 0,
|
||||
# Stored unencrypted, so an opaque ID rather than personal data.
|
||||
"fairness_group": ctx.user["org_id"],
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
__slots__ = ("_assign_handler",)
|
||||
|
||||
types = types
|
||||
"""Reference to priority type definitions.
|
||||
|
||||
Provides access to the type definitions used by the priority system:
|
||||
PriorityContext and PrioritySettings.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Priority instance."""
|
||||
_warn_priority_beta()
|
||||
self._assign_handler: _AssignHandler | None = None
|
||||
|
||||
def assign(self, fn: _AssignHandlerT) -> _AssignHandlerT:
|
||||
"""Register the function that assigns a thread's priority settings.
|
||||
|
||||
The function receives a `PriorityContext` and returns `PrioritySettings`,
|
||||
or `None` for the defaults.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@priority.assign
|
||||
async def assign(ctx: PriorityContext) -> PrioritySettings | None:
|
||||
if ctx.cron_id is not None:
|
||||
return {"priority": -2}
|
||||
return None
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The assign handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
|
||||
Raises:
|
||||
DuplicateHandlerError: If an assign handler is already registered
|
||||
TypeError: If the handler has an invalid signature
|
||||
"""
|
||||
if self._assign_handler is not None:
|
||||
raise DuplicateHandlerError("Priority assign handler already registered")
|
||||
_validate_handler(fn)
|
||||
self._assign_handler = fn
|
||||
return fn
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Priority types for LangGraph.
|
||||
|
||||
This module defines the context the server passes to the function registered
|
||||
with `@priority.assign`, and the settings that function returns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
from langgraph_sdk.auth.types import BaseUser
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PriorityContext:
|
||||
"""Context passed to the `@priority.assign` function at the first run on a thread with no settings."""
|
||||
|
||||
user: BaseUser
|
||||
"""The user from the deployment's auth, custom auth or LangSmith auth. Without auth, `user.identity` is empty."""
|
||||
|
||||
permissions: Sequence[str]
|
||||
"""The user's permissions from the same auth. Without auth, it's empty."""
|
||||
|
||||
thread_id: str | None
|
||||
"""The thread's ID, or `None` when the run creates its thread without being given an ID."""
|
||||
|
||||
thread_metadata: dict[str, typing.Any]
|
||||
"""The thread's metadata."""
|
||||
|
||||
assistant_id: str
|
||||
"""The ID of the assistant the run uses."""
|
||||
|
||||
graph_id: str
|
||||
"""The ID of the graph the assistant runs."""
|
||||
|
||||
assistant_metadata: dict[str, typing.Any]
|
||||
"""The assistant's metadata."""
|
||||
|
||||
run_metadata: dict[str, typing.Any]
|
||||
"""The run's metadata."""
|
||||
|
||||
cron_id: str | None
|
||||
"""The ID of the cron that started the run, or `None` when no cron started it."""
|
||||
|
||||
|
||||
class PrioritySettings(typing.TypedDict, total=False):
|
||||
"""Priority settings returned by the `@priority.assign` function.
|
||||
|
||||
Every key is optional: a missing key or a key set to `None` takes its
|
||||
default, and returning `None` instead of a dict gives every key its default.
|
||||
A value outside its range, or an empty `fairness_group`, is an error.
|
||||
"""
|
||||
|
||||
priority: int | None
|
||||
"""An integer from -5 to 5, where 5 runs first and -5 runs last. Defaults to 0."""
|
||||
|
||||
fairness_group: str | None
|
||||
"""The thread's fairness group, a non-empty string. Defaults to `"default"`.
|
||||
|
||||
The server stores it unencrypted, even when the deployment uses custom
|
||||
encryption, so use an opaque ID rather than personal data.
|
||||
"""
|
||||
|
||||
fairness_weight: int | None
|
||||
"""The weight of the thread's fairness group, an integer from 1 to 100. Defaults to 1."""
|
||||
@@ -16,7 +16,7 @@ dependencies = [
|
||||
"orjson>=3.11.5",
|
||||
"langchain-protocol>=0.0.15",
|
||||
"langchain-core>=1.4.0,<2",
|
||||
"websockets>=14",
|
||||
"websockets>=14,<17",
|
||||
]
|
||||
|
||||
[tool.hatch.version]
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
import langgraph_sdk
|
||||
from langgraph_sdk import Priority, PriorityContext, PrioritySettings
|
||||
from langgraph_sdk.encryption import DuplicateHandlerError, LangGraphBetaWarning
|
||||
from langgraph_sdk.priority import _warn_priority_beta
|
||||
|
||||
|
||||
class _User:
|
||||
is_authenticated = True
|
||||
display_name = "user-1"
|
||||
identity = "user-1"
|
||||
permissions: tuple[str, ...] = ()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return hasattr(self, key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(("identity", "display_name", "is_authenticated", "permissions"))
|
||||
|
||||
|
||||
def _context(thread_id: str | None) -> PriorityContext:
|
||||
return PriorityContext(
|
||||
user=_User(),
|
||||
permissions=[],
|
||||
thread_id=thread_id,
|
||||
thread_metadata={},
|
||||
assistant_id="assistant-1",
|
||||
graph_id="agent",
|
||||
assistant_metadata={"tier": "gold"},
|
||||
run_metadata={},
|
||||
cron_id=None,
|
||||
)
|
||||
|
||||
|
||||
async def _no_params() -> PrioritySettings | None:
|
||||
return None
|
||||
|
||||
|
||||
async def _two_params(_ctx: PriorityContext, _extra: object) -> PrioritySettings | None:
|
||||
return None
|
||||
|
||||
|
||||
def test_priority_and_its_types_are_exported():
|
||||
assert {"Priority", "PriorityContext", "PrioritySettings"} <= set(
|
||||
langgraph_sdk.__all__
|
||||
)
|
||||
assert Priority.types.PriorityContext is PriorityContext
|
||||
assert Priority.types.PrioritySettings is PrioritySettings
|
||||
|
||||
|
||||
def test_construction_warns_that_priority_is_beta():
|
||||
# The warning fires once per process, so an earlier test may have used it up.
|
||||
_warn_priority_beta.cache_clear()
|
||||
with pytest.warns(LangGraphBetaWarning, match="Priority API is in beta"):
|
||||
Priority()
|
||||
|
||||
|
||||
def test_assign_registers_and_returns_the_function():
|
||||
priority = Priority()
|
||||
|
||||
async def assign(_ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return {"priority": 3, "fairness_group": "org-42"}
|
||||
|
||||
assert priority.assign(assign) is assign
|
||||
assert priority._assign_handler is assign
|
||||
|
||||
|
||||
def test_assign_rejects_a_sync_function():
|
||||
priority = Priority()
|
||||
|
||||
def assign(_ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return None
|
||||
|
||||
with pytest.raises(TypeError, match="must be an async function"):
|
||||
priority.assign(assign) # ty: ignore[invalid-argument-type]
|
||||
assert priority._assign_handler is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fn", [_no_params, _two_params])
|
||||
def test_assign_rejects_the_wrong_number_of_parameters(fn):
|
||||
priority = Priority()
|
||||
|
||||
with pytest.raises(TypeError, match="must accept exactly 1 parameter"):
|
||||
priority.assign(fn)
|
||||
assert priority._assign_handler is None
|
||||
|
||||
|
||||
def test_assign_rejects_a_second_function():
|
||||
priority = Priority()
|
||||
|
||||
@priority.assign
|
||||
async def first(_ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return None
|
||||
|
||||
with pytest.raises(DuplicateHandlerError):
|
||||
|
||||
@priority.assign
|
||||
async def second(_ctx: PriorityContext) -> PrioritySettings | None:
|
||||
return None
|
||||
|
||||
assert priority._assign_handler is first
|
||||
|
||||
|
||||
def test_context_accepts_no_thread_id():
|
||||
# `make lint` type-checks this call too, so `thread_id` must accept None there.
|
||||
assert _context(thread_id=None).thread_id is None
|
||||
|
||||
|
||||
def test_context_is_frozen():
|
||||
ctx = _context(thread_id="thread-1")
|
||||
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
ctx.thread_id = "thread-2" # ty: ignore[invalid-assignment]
|
||||
|
||||
|
||||
def test_every_settings_key_is_optional():
|
||||
assert PrioritySettings.__optional_keys__ == {
|
||||
"priority",
|
||||
"fairness_group",
|
||||
"fairness_weight",
|
||||
}
|
||||
assert PrioritySettings.__required_keys__ == frozenset()
|
||||
Generated
+1
-1
@@ -525,7 +525,7 @@ requires-dist = [
|
||||
{ name = "langchain-core", specifier = ">=1.4.0,<2" },
|
||||
{ name = "langchain-protocol", specifier = ">=0.0.15" },
|
||||
{ name = "orjson", specifier = ">=3.11.5" },
|
||||
{ name = "websockets", specifier = ">=14" },
|
||||
{ name = "websockets", specifier = ">=14,<17" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
Reference in New Issue
Block a user