Compare commits

..
5 changed files with 117 additions and 136 deletions
-4
View File
@@ -7,10 +7,6 @@ on:
paths: paths:
- 'docs/**' - 'docs/**'
- '.github/workflows/deploy-redirects.yml' - '.github/workflows/deploy-redirects.yml'
# llms.txt is fetched from docs.langchain.com at build time, so redeploy on a
# schedule to pick up docs changes that never touch this repo.
schedule:
- cron: '17 6 * * 1'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
+7 -83
View File
@@ -12,26 +12,13 @@ which is SEO-friendly and treated similarly to 301 redirects by Google.
To add new redirects, simply edit redirects.json and re-run this script. To add new redirects, simply edit redirects.json and re-run this script.
""" """
import http.client
import json import json
import os import os
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path from pathlib import Path
# Default fallback URL for any path not in the redirect map # Default fallback URL for any path not in the redirect map
DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview" DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview"
# The docs site regenerates this index on every deploy, so fetching it here
# keeps the published llms.txt from drifting. The URL is a hardcoded constant,
# never built from input, and both it and the post-redirect URL are checked
# against ALLOWED_LLMS_HOST before anything is read.
CANONICAL_LLMS_URL = "https://docs.langchain.com/oss/python/langgraph/llms.txt"
ALLOWED_LLMS_HOST = "docs.langchain.com"
LLMS_FETCH_TIMEOUT = 30
LLMS_MAX_BYTES = 1_000_000
HTML_TEMPLATE = """<!doctype html> HTML_TEMPLATE = """<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -88,64 +75,6 @@ CATCHALL_404_TEMPLATE = """<!doctype html>
""" """
def is_allowed_llms_url(url):
"""Return True if url is HTTPS on the one host we accept content from."""
parsed = urllib.parse.urlsplit(url)
return parsed.scheme == "https" and parsed.hostname == ALLOWED_LLMS_HOST
def fetch_canonical_llms_txt():
"""Return the published LangGraph index, or None if it cannot be used.
Returning None leaves the caller on the committed docs/llms.txt, so a
docs.langchain.com outage degrades to a stale file rather than a broken
deploy or a published error page.
"""
if not is_allowed_llms_url(CANONICAL_LLMS_URL):
print(f"Refusing to fetch {CANONICAL_LLMS_URL}: host not allowed")
return None
try:
with urllib.request.urlopen( # noqa: S310 - constant, allowlisted URL
CANONICAL_LLMS_URL, timeout=LLMS_FETCH_TIMEOUT
) as response:
# urlopen follows redirects, so re-check where it actually landed.
if not is_allowed_llms_url(response.url):
print(f"Refusing {CANONICAL_LLMS_URL}: redirected to {response.url}")
return None
body = response.read(LLMS_MAX_BYTES + 1)
# A connection dropped mid-body raises http.client.IncompleteRead, which
# descends from HTTPException rather than OSError, so catching only the
# urllib and OS errors would let it escape and fail the whole deploy.
except (
urllib.error.URLError,
http.client.HTTPException,
TimeoutError,
OSError,
) as exc:
print(f"Could not fetch {CANONICAL_LLMS_URL}: {type(exc).__name__}: {exc}")
return None
if len(body) > LLMS_MAX_BYTES:
print(f"Refusing {CANONICAL_LLMS_URL}: larger than {LLMS_MAX_BYTES} bytes")
return None
try:
text = body.decode("utf-8")
except UnicodeDecodeError as exc:
print(f"Refusing {CANONICAL_LLMS_URL}: not valid UTF-8: {exc}")
return None
# An index opens with a markdown heading and links to the docs site. A
# body that does not is an error page or a truncated response, not content
# worth publishing.
if not text.startswith("# ") or f"https://{ALLOWED_LLMS_HOST}/" not in text:
print(f"Refusing {CANONICAL_LLMS_URL}: does not look like an llms.txt index")
return None
return text
def generate_redirects(): def generate_redirects():
script_dir = Path(__file__).parent script_dir = Path(__file__).parent
output_dir = script_dir / "_site" output_dir = script_dir / "_site"
@@ -197,19 +126,14 @@ def generate_redirects():
catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT)) catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT))
print(f"Created: {catchall_404}") print(f"Created: {catchall_404}")
# llms.txt can't be redirected via HTML, so publish the docs site's own # Copy static files (like llms.txt) that can't be redirected via HTML
# generated index. The committed copy is only a fallback. static_files = ["llms.txt"]
llms_txt = fetch_canonical_llms_txt() for static_file in static_files:
if llms_txt is not None: src = script_dir / static_file
(output_dir / "llms.txt").write_text(llms_txt)
print(f"Fetched: {output_dir / 'llms.txt'} (from {CANONICAL_LLMS_URL})")
else:
src = script_dir / "llms.txt"
if src.exists(): if src.exists():
(output_dir / "llms.txt").write_text(src.read_text()) dst = output_dir / static_file
print(f"Copied: {output_dir / 'llms.txt'} (fallback, may be stale)") dst.write_text(src.read_text())
else: print(f"Copied: {dst}")
print("No llms.txt fetched and no committed fallback; skipping")
print(f"\nGenerated {len(redirects)} redirect files in {output_dir}") print(f"\nGenerated {len(redirects)} redirect files in {output_dir}")
+32 -46
View File
@@ -1,49 +1,35 @@
# Docs by LangChain: LangGraph (Python) # LangGraph
> Markdown index of the LangGraph (Python) documentation. LangGraph documentation has moved to docs.langchain.com.
## LangGraph (Python) ## Overview
- [Memory](https://docs.langchain.com/oss/python/langgraph/add-memory.md) - [LangGraph Overview](https://docs.langchain.com/oss/python/langgraph/overview): Introduction to LangGraph, a library for building stateful, multi-actor applications with LLMs.
- [Build a custom RAG agent with LangGraph](https://docs.langchain.com/oss/python/langgraph/agentic-rag.md) - [Why LangGraph?](https://docs.langchain.com/oss/python/langgraph/why-langgraph): Motivation for LangGraph and its key features.
- [Application structure](https://docs.langchain.com/oss/python/langgraph/application-structure.md)
- [Backward compatibility](https://docs.langchain.com/oss/python/langgraph/backward-compatibility.md) ## Core Concepts
- [Case studies](https://docs.langchain.com/oss/python/langgraph/case-studies.md)
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-js.md) - [Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api): Learn how to define state, create nodes, and connect them with edges.
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-py.md) - [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming): Stream outputs from your graph for better UX.
- [Checkpointers](https://docs.langchain.com/oss/python/langgraph/checkpointers.md) - [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence): Add memory and checkpointing to your graphs.
- [Choosing between Graph and Functional APIs](https://docs.langchain.com/oss/python/langgraph/choosing-apis.md) - [Add Memory](https://docs.langchain.com/oss/python/langgraph/add-memory): Implement short-term and long-term memory.
- [Deployment](https://docs.langchain.com/oss/python/langgraph/deploy.md) - [Workflows & Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents): Build agents and workflows with LangGraph.
- [GRAPH_RECURSION_LIMIT](https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT.md)
- [INVALID_CHAT_HISTORY](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CHAT_HISTORY.md) ## How-To Guides
- [INVALID_CONCURRENT_GRAPH_UPDATE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md)
- [INVALID_GRAPH_NODE_RETURN_VALUE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md) - [Use Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs): Compose graphs using subgraphs.
- [MISSING_CHECKPOINTER](https://docs.langchain.com/oss/python/langgraph/errors/MISSING_CHECKPOINTER.md) - [Observability](https://docs.langchain.com/oss/python/langgraph/observability): Add tracing and debugging to your graphs.
- [MULTIPLE_SUBGRAPHS](https://docs.langchain.com/oss/python/langgraph/errors/MULTIPLE_SUBGRAPHS.md) - [Common Errors](https://docs.langchain.com/oss/python/langgraph/common-errors): Troubleshoot common LangGraph errors.
- [Event streaming](https://docs.langchain.com/oss/python/langgraph/event-streaming.md)
- [Fault tolerance](https://docs.langchain.com/oss/python/langgraph/fault-tolerance.md) ## Tutorials
- [Custom stream channels](https://docs.langchain.com/oss/python/langgraph/frontend/custom-stream-channels.md)
- [Graph execution](https://docs.langchain.com/oss/python/langgraph/frontend/graph-execution.md) - [Agentic RAG](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Build an agentic RAG system with LangGraph.
- [Overview](https://docs.langchain.com/oss/python/langgraph/frontend/overview.md) - [SQL Agent](https://docs.langchain.com/oss/python/langgraph/sql-agent): Create a SQL agent with LangGraph.
- [Functional API overview](https://docs.langchain.com/oss/python/langgraph/functional-api.md)
- [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api.md) ## Reference
- [Install LangGraph](https://docs.langchain.com/oss/python/langgraph/install.md)
- [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts.md) - [API Reference](https://reference.langchain.com/python/langgraph/): Complete API documentation for LangGraph.
- [Run a local server](https://docs.langchain.com/oss/python/langgraph/local-server.md)
- [LangSmith Observability](https://docs.langchain.com/oss/python/langgraph/observability.md) ## LangGraph Platform
- [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview.md)
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence.md) For deploying LangGraph applications in production, see the [LangSmith documentation](https://docs.langchain.com/langsmith/agent-server).
- [LangGraph runtime](https://docs.langchain.com/oss/python/langgraph/pregel.md)
- [Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart.md)
- [Build a custom SQL agent](https://docs.langchain.com/oss/python/langgraph/sql-agent.md)
- [Stores](https://docs.langchain.com/oss/python/langgraph/stores.md)
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming.md)
- [LangSmith Studio](https://docs.langchain.com/oss/python/langgraph/studio.md)
- [Test](https://docs.langchain.com/oss/python/langgraph/test.md)
- [Thinking in LangGraph](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph.md)
- [Agent Chat UI](https://docs.langchain.com/oss/python/langgraph/ui.md)
- [Use the functional API](https://docs.langchain.com/oss/python/langgraph/use-functional-api.md)
- [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api.md)
- [Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs.md)
- [Use time-travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel.md)
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents.md)
+20 -3
View File
@@ -51,6 +51,7 @@ RESERVED_ENV_VARS = frozenset(
"LANGGRAPH_AUTH_TYPE", "LANGGRAPH_AUTH_TYPE",
"LANGSMITH_AUTH_ENDPOINT", "LANGSMITH_AUTH_ENDPOINT",
"LANGSMITH_TENANT_ID", "LANGSMITH_TENANT_ID",
"LANGSMITH_WORKSPACE_ID",
"LANGSMITH_AUTH_VERIFY_TENANT_ID", "LANGSMITH_AUTH_VERIFY_TENANT_ID",
"LANGSMITH_HOST_PROJECT_ID", "LANGSMITH_HOST_PROJECT_ID",
"LANGSMITH_HOST_PROJECT_NAME", "LANGSMITH_HOST_PROJECT_NAME",
@@ -1229,6 +1230,24 @@ def _run_remote_build(
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _get_tenant_id(env_vars: dict[str, str]) -> str | None:
"""Get the tenant ID from LANGSMITH_TENANT_ID or LANGSMITH_WORKSPACE_ID."""
tenant_id = env_vars.get("LANGSMITH_TENANT_ID") or os.environ.get(
"LANGSMITH_TENANT_ID"
)
fallback_tenant_id = env_vars.get("LANGSMITH_WORKSPACE_ID") or os.environ.get(
"LANGSMITH_WORKSPACE_ID"
)
if tenant_id and fallback_tenant_id:
raise click.UsageError(
"LANGSMITH_TENANT_ID and LANGSMITH_WORKSPACE_ID cannot both be set. "
"Set only one."
)
if tenant_id:
return tenant_id
return fallback_tenant_id or None
def _create_host_backend_client( def _create_host_backend_client(
host_url: str | None, host_url: str | None,
api_key: str | None, api_key: str | None,
@@ -1236,6 +1255,7 @@ def _create_host_backend_client(
) -> HostBackendClient: ) -> HostBackendClient:
if env_vars is None: if env_vars is None:
env_vars = _parse_env_from_config({}, pathlib.Path.cwd() / DEFAULT_CONFIG) env_vars = _parse_env_from_config({}, pathlib.Path.cwd() / DEFAULT_CONFIG)
tenant_id = _get_tenant_id(env_vars)
resolved_api_key = api_key resolved_api_key = api_key
if not resolved_api_key: if not resolved_api_key:
for key_name in _API_KEY_ENV_NAMES: for key_name in _API_KEY_ENV_NAMES:
@@ -1258,9 +1278,6 @@ def _create_host_backend_client(
fg="yellow", fg="yellow",
) )
resolved_api_key = click.prompt("Enter LangSmith API key", hide_input=True) resolved_api_key = click.prompt("Enter LangSmith API key", hide_input=True)
tenant_id = env_vars.get("LANGSMITH_TENANT_ID") or os.environ.get(
"LANGSMITH_TENANT_ID"
)
return HostBackendClient(host_url, resolved_api_key, tenant_id=tenant_id) return HostBackendClient(host_url, resolved_api_key, tenant_id=tenant_id)
@@ -21,6 +21,7 @@ from langgraph_cli.deploy import (
_parse_env_from_config, _parse_env_from_config,
_resolve_env_path, _resolve_env_path,
_resolve_pushed_image_digest, _resolve_pushed_image_digest,
_secrets_from_env,
_smith_dashboard_base_url, _smith_dashboard_base_url,
_validate_prebuilt_image, _validate_prebuilt_image,
normalize_image_tag, normalize_image_tag,
@@ -540,6 +541,63 @@ class TestCreateHostBackendClientNoInput:
assert client is not None assert client is not None
@pytest.mark.parametrize("source", ["config", "shell"])
@pytest.mark.parametrize("name", ["LANGSMITH_TENANT_ID", "LANGSMITH_WORKSPACE_ID"])
def test_workspace_id_alias(monkeypatch, source, name):
monkeypatch.delenv("LANGSMITH_WORKSPACE_ID", raising=False)
monkeypatch.delenv("LANGSMITH_TENANT_ID", raising=False)
env_vars = {}
if source == "config":
env_vars[name] = "workspace"
else:
monkeypatch.setenv(name, "workspace")
def handler(request):
assert request.headers["X-Tenant-ID"] == "workspace"
return httpx.Response(200, json={"resources": []})
monkeypatch.setattr(
httpx, "HTTPTransport", lambda **kwargs: httpx.MockTransport(handler)
)
client = _create_host_backend_client(
"https://api.example.com", "test-key", env_vars
)
try:
client.list_deployments()
finally:
client._client.close()
@pytest.mark.parametrize("tenant_source", ["config", "shell"])
@pytest.mark.parametrize("workspace_source", ["config", "shell"])
@pytest.mark.parametrize("workspace_id", ["tenant-id", "workspace-id"])
def test_rejects_both_workspace_names(
monkeypatch, tenant_source, workspace_source, workspace_id
):
env_vars = {}
for name, source, value in [
("LANGSMITH_TENANT_ID", tenant_source, "tenant-id"),
("LANGSMITH_WORKSPACE_ID", workspace_source, workspace_id),
]:
monkeypatch.delenv(name, raising=False)
if source == "config":
env_vars[name] = value
else:
monkeypatch.setenv(name, value)
with pytest.raises(
click.UsageError,
match="LANGSMITH_TENANT_ID and LANGSMITH_WORKSPACE_ID cannot both be set",
):
_create_host_backend_client("https://api.example.com", "test-key", env_vars)
def test_workspace_id_is_not_uploaded_as_secret():
assert _secrets_from_env(
{"LANGSMITH_WORKSPACE_ID": "workspace", "APP_SETTING": "value"}
) == [{"name": "APP_SETTING", "value": "value"}]
class TestSmithDashboardBaseUrl: class TestSmithDashboardBaseUrl:
def test_none_returns_default(self): def test_none_returns_default(self):
assert _smith_dashboard_base_url(None) == "https://smith.langchain.com" assert _smith_dashboard_base_url(None) == "https://smith.langchain.com"