Files
wehub-resource-sync a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:18 +08:00

297 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Script to generate a JSON schema for the langgraph-cli Config class.
This script creates a schema.json file that can be referenced in langgraph.json files
to provide IDE autocompletion and validation.
"""
import inspect
import json
import textwrap
from pathlib import Path
import msgspec
from langgraph_cli.schemas import (
AuthConfig,
CacheConfig,
CheckpointerConfig,
Config,
ConfigurableHeaderConfig,
CorsConfig,
GraphDef,
HttpConfig,
IndexConfig,
SecurityConfig,
SerdeConfig,
StoreConfig,
ThreadTTLConfig,
TTLConfig,
WebhooksConfig,
WebhookUrlPolicy,
)
def add_descriptions_to_schema(schema, cls):
"""Add docstring descriptions to the schema properties."""
if schema.get("description"):
schema["description"] = inspect.cleandoc(schema["description"])
elif class_doc := inspect.getdoc(cls):
schema["description"] = inspect.cleandoc(class_doc)
# Get attribute docstrings from the class
attr_docs = {}
# Also check class annotations for docstrings
source_lines = inspect.getsourcelines(cls)[0]
current_attr = None
docstring_lines = []
for line in source_lines:
line = line.strip()
# Check for attribute definition (TypedDict style)
if ":" in line and not line.startswith("#") and not line.startswith('"""'):
parts = line.split(":", 1)
if len(parts) == 2 and parts[0].strip().isidentifier():
# If we were collecting a docstring, save it for the previous attribute
if current_attr and docstring_lines:
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
docstring_lines = []
current_attr = parts[0].strip()
# Check for docstring after attribute
elif line.startswith('"""') and current_attr:
# Start or end of a docstring
if len(line) > 3 and line.endswith('"""'):
# Single line docstring
attr_docs[current_attr] = line.strip('"')
current_attr = None
elif docstring_lines:
# End of multi-line docstring
docstring_lines.append(line.rstrip('"'))
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
docstring_lines = []
current_attr = None
else:
# Start of multi-line docstring
docstring_lines.append(line.lstrip('"'))
# Continue multi-line docstring
elif docstring_lines and current_attr:
docstring_lines.append(line.strip('"'))
# Add the last docstring if there is one
if current_attr and docstring_lines:
attr_docs[current_attr] = "\n".join(docstring_lines).strip('"')
# Add descriptions to properties
if "properties" in schema:
for prop_name, prop_schema in schema["properties"].items():
# First try to get from attribute docstrings
if prop_name in attr_docs and "description" not in prop_schema:
prop_schema["description"] = textwrap.dedent(attr_docs[prop_name])
# Fall back to class docstring parsing
elif class_doc:
for line in class_doc.split("\n"):
if line.strip().startswith(
f"{prop_name}:"
) or line.strip().startswith(f'"{prop_name}"'):
description = line.split(":", 1)[1].strip()
if description and "description" not in prop_schema:
prop_schema["description"] = description
break
# Recursively process nested definitions
if "$defs" in schema:
for def_name, def_schema in schema["$defs"].items():
# Find the class that corresponds to this definition
for potential_cls in [
Config,
GraphDef,
StoreConfig,
IndexConfig,
AuthConfig,
SecurityConfig,
HttpConfig,
CorsConfig,
CacheConfig,
ThreadTTLConfig,
CheckpointerConfig,
SerdeConfig,
TTLConfig,
ConfigurableHeaderConfig,
WebhooksConfig,
WebhookUrlPolicy,
]:
if potential_cls.__name__ == def_name:
add_descriptions_to_schema(def_schema, potential_cls)
break
return schema
def generate_schema():
"""Generate a JSON schema for the Config class using msgspec."""
# Generate the basic schema
schema = msgspec.json.schema(Config)
# Add title and description
schema["title"] = "LangGraph CLI Configuration"
schema["description"] = "Configuration schema for langgraph-cli"
# Add docstring descriptions
schema = add_descriptions_to_schema(schema, Config)
# Add constraint that only one of python_version or node_version should be specified
config_schema = schema["$defs"]["Config"]
# Create two subschemas: one with python_version and one with node_version
# Define properties specific to Python projects
python_specific_props = ["python_version", "pip_config_file"]
# Define properties specific to Node.js projects
node_specific_props = ["node_version"]
# Define properties common to both project types
common_props = [
k
for k in config_schema["properties"]
if k not in python_specific_props and k not in node_specific_props
]
# Create legacy Python schema with python_version and pip_config_file
legacy_python_schema = {
"type": "object",
"properties": {
# Include Python-specific properties
**{k: config_schema["properties"][k].copy() for k in python_specific_props},
# Include common properties
**{k: config_schema["properties"][k].copy() for k in common_props},
},
"required": ["dependencies", "graphs"],
}
legacy_python_schema["properties"]["pip_installer"] = {
"anyOf": [
{"type": "string", "enum": ["auto", "pip", "uv"]},
{"type": "null"},
]
}
uv_source_python_schema = {
"type": "object",
"properties": {
**{
k: config_schema["properties"][k].copy()
for k in python_specific_props + common_props
},
},
"required": ["graphs", "source"],
}
# source must be a UvSource object (not null)
uv_source_python_schema["properties"]["source"] = {"$ref": "#/$defs/UvSource"}
uv_source_python_schema["properties"]["pip_installer"] = {
"anyOf": [
{"type": "string", "enum": ["auto", "pip", "uv"]},
{"type": "null"},
]
}
# Add enum constraint for python_version
if "python_version" in legacy_python_schema["properties"]:
legacy_python_schema["properties"]["python_version"]["enum"] = [
"3.11",
"3.12",
"3.13",
]
if "python_version" in uv_source_python_schema["properties"]:
uv_source_python_schema["properties"]["python_version"]["enum"] = [
"3.11",
"3.12",
"3.13",
]
# Create Node.js schema with node_version
node_schema = {
"type": "object",
"properties": {
# Include Node-specific properties
**{k: config_schema["properties"][k].copy() for k in node_specific_props},
# Include common properties
**{k: config_schema["properties"][k].copy() for k in common_props},
},
"required": ["node_version", "graphs"],
}
node_schema["properties"]["pip_installer"] = {
"anyOf": [
{"type": "string", "enum": ["auto", "pip", "uv"]},
{"type": "null"},
]
}
# Add enum constraint for node_version
if "node_version" in node_schema["properties"]:
node_schema["properties"]["node_version"]["anyOf"] = [
{"type": "string", "enum": ["20"]},
{"type": "null"},
]
# Add enum constraint for image_distro
if "image_distro" in node_schema["properties"]:
node_schema["properties"]["image_distro"]["anyOf"] = [
{"type": "string", "enum": ["debian", "wolfi"]},
{"type": "null"},
]
# Replace the Config schema with a oneOf constraint
config_schema["oneOf"] = [
legacy_python_schema,
uv_source_python_schema,
node_schema,
]
# Remove the properties field as it's now defined in the oneOf subschemas
if "properties" in config_schema:
del config_schema["properties"]
return schema
def main():
"""Generate the schema and write it to a file."""
schema = generate_schema()
# Add versioning to the schema
import importlib.metadata
try:
version = importlib.metadata.version("langgraph_cli").split(".")
schema_version = f"v{version[0]}"
except importlib.metadata.PackageNotFoundError:
schema_version = "v1"
# Add version to schema
schema["version"] = schema_version
config_dir = Path(__file__).parent / "schemas"
# Create versioned schema file
versioned_path = config_dir / f"schema.{schema_version}.json"
with open(versioned_path, "w") as f:
json.dump(schema, f, indent=2)
# Also create a latest version
latest_path = config_dir / "schema.json"
with open(latest_path, "w") as f:
json.dump(schema, f, indent=2)
print(f"Schema written to {versioned_path} and {latest_path}")
print(
f"You can now add '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.json'"
f" or '$schema: https://raw.githubusercontent.com/langchain-ai/langgraph/refs/heads/main/libs/cli/schemas/schema.{schema_version}.json'"
" to your langgraph.json files"
)
if __name__ == "__main__":
main()