Skip to content

Commit 230927f

Browse files
authored
docs: publish the generated llms.txt instead of a hand-written copy (#8922)
## Why `docs/llms.txt` is written by hand and was last touched in February, so the index served at <https://langchain-ai.github.io/langgraph/llms.txt> had drifted badly: - 12 pages listed, against the 43 the docs site publishes. - One entry, `/oss/python/langgraph/why-langgraph`, now 404s. The deploy workflow only fires on `docs/**` pushes, so nothing brought the file back in line as the docs site changed. It is served verbatim: `generate_redirects.py` copies it into `docs/_site` via a `static_files` list, and `deploy-redirects.yml` uploads that directory to Pages. The docs site already generates exactly this index, and regenerates it on every deploy: ``` https://docs.langchain.com/oss/python/langgraph/llms.txt ``` ## What changed `generate_redirects.py` now fetches that index at build time and writes it into `_site`, so the published file cannot drift from the docs site. The committed `docs/llms.txt` stays as a fallback only, refreshed here to current content. `deploy-redirects.yml` gains a weekly `schedule:` trigger, so docs changes that never touch this repo still reach the deployed file. The `permissions:` block is unchanged. ## Fetching remote content in CI The fetched body is published on a public Pages site, so it is validated before it is written: - The URL is a hardcoded module constant, never built from input. - Both that URL and the post-redirect `response.url` are checked against an HTTPS-plus-single-host allowlist. `urlopen` follows redirects, so checking only the request URL would not be enough. - 30 second timeout, response capped at 1MB, decoded as UTF-8. - The body must open with a markdown heading and contain a docs.langchain.com link, which rejects an error page or a truncated response. Any failure returns `None` and falls back to the committed copy, so a docs.langchain.com outage degrades to a stale file rather than a broken deploy or a published error page. ## Verification Ran `python docs/generate_redirects.py`: 294 redirect files, `llms.txt` fetched, 43 entries. Each rejection path was exercised against a stubbed `urlopen` and all fall back to the committed file: | Case | Result | |---|---| | Network error | falls back | | Timeout | falls back | | Redirect to another host | falls back | | Body over 1MB | falls back | | Invalid UTF-8 | falls back | | HTML error page | falls back | | Empty body | falls back | | Valid index | published | Allowlist rejects `http://docs.langchain.com/...`, `https://evil.com/...`, and `https://docs.langchain.com.evil.com/...`. ## Note on ordering The fallback committed here is labelled "LangGraph (Python)", which is what the docs site will serve once langchain-ai/docs#6032 deploys. Until then the fetched file reads "Open source (Python)". No action needed; it self-corrects. --- Written with Claude Code; I reviewed the diff and ran the verification above.
1 parent e539ac1 commit 230927f

3 files changed

Lines changed: 136 additions & 42 deletions

File tree

‎.github/workflows/deploy-redirects.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
paths:
88
- 'docs/**'
99
- '.github/workflows/deploy-redirects.yml'
10+
# llms.txt is fetched from docs.langchain.com at build time, so redeploy on a
11+
# schedule to pick up docs changes that never touch this repo.
12+
schedule:
13+
- cron: '17 6 * * 1'
1014
workflow_dispatch:
1115

1216
permissions:

‎docs/generate_redirects.py‎

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,26 @@
1212
To add new redirects, simply edit redirects.json and re-run this script.
1313
"""
1414

15+
import http.client
1516
import json
1617
import os
18+
import urllib.error
19+
import urllib.parse
20+
import urllib.request
1721
from pathlib import Path
1822

1923
# Default fallback URL for any path not in the redirect map
2024
DEFAULT_REDIRECT = "https://docs.langchain.com/oss/python/langgraph/overview"
2125

26+
# The docs site regenerates this index on every deploy, so fetching it here
27+
# keeps the published llms.txt from drifting. The URL is a hardcoded constant,
28+
# never built from input, and both it and the post-redirect URL are checked
29+
# against ALLOWED_LLMS_HOST before anything is read.
30+
CANONICAL_LLMS_URL = "https://docs.langchain.com/oss/python/langgraph/llms.txt"
31+
ALLOWED_LLMS_HOST = "docs.langchain.com"
32+
LLMS_FETCH_TIMEOUT = 30
33+
LLMS_MAX_BYTES = 1_000_000
34+
2235
HTML_TEMPLATE = """<!doctype html>
2336
<html lang="en">
2437
<head>
@@ -75,6 +88,64 @@
7588
"""
7689

7790

91+
def is_allowed_llms_url(url):
92+
"""Return True if url is HTTPS on the one host we accept content from."""
93+
parsed = urllib.parse.urlsplit(url)
94+
return parsed.scheme == "https" and parsed.hostname == ALLOWED_LLMS_HOST
95+
96+
97+
def fetch_canonical_llms_txt():
98+
"""Return the published LangGraph index, or None if it cannot be used.
99+
100+
Returning None leaves the caller on the committed docs/llms.txt, so a
101+
docs.langchain.com outage degrades to a stale file rather than a broken
102+
deploy or a published error page.
103+
"""
104+
if not is_allowed_llms_url(CANONICAL_LLMS_URL):
105+
print(f"Refusing to fetch {CANONICAL_LLMS_URL}: host not allowed")
106+
return None
107+
108+
try:
109+
with urllib.request.urlopen( # noqa: S310 - constant, allowlisted URL
110+
CANONICAL_LLMS_URL, timeout=LLMS_FETCH_TIMEOUT
111+
) as response:
112+
# urlopen follows redirects, so re-check where it actually landed.
113+
if not is_allowed_llms_url(response.url):
114+
print(f"Refusing {CANONICAL_LLMS_URL}: redirected to {response.url}")
115+
return None
116+
body = response.read(LLMS_MAX_BYTES + 1)
117+
# A connection dropped mid-body raises http.client.IncompleteRead, which
118+
# descends from HTTPException rather than OSError, so catching only the
119+
# urllib and OS errors would let it escape and fail the whole deploy.
120+
except (
121+
urllib.error.URLError,
122+
http.client.HTTPException,
123+
TimeoutError,
124+
OSError,
125+
) as exc:
126+
print(f"Could not fetch {CANONICAL_LLMS_URL}: {type(exc).__name__}: {exc}")
127+
return None
128+
129+
if len(body) > LLMS_MAX_BYTES:
130+
print(f"Refusing {CANONICAL_LLMS_URL}: larger than {LLMS_MAX_BYTES} bytes")
131+
return None
132+
133+
try:
134+
text = body.decode("utf-8")
135+
except UnicodeDecodeError as exc:
136+
print(f"Refusing {CANONICAL_LLMS_URL}: not valid UTF-8: {exc}")
137+
return None
138+
139+
# An index opens with a markdown heading and links to the docs site. A
140+
# body that does not is an error page or a truncated response, not content
141+
# worth publishing.
142+
if not text.startswith("# ") or f"https://{ALLOWED_LLMS_HOST}/" not in text:
143+
print(f"Refusing {CANONICAL_LLMS_URL}: does not look like an llms.txt index")
144+
return None
145+
146+
return text
147+
148+
78149
def generate_redirects():
79150
script_dir = Path(__file__).parent
80151
output_dir = script_dir / "_site"
@@ -126,14 +197,19 @@ def generate_redirects():
126197
catchall_404.write_text(CATCHALL_404_TEMPLATE.format(default_url=DEFAULT_REDIRECT))
127198
print(f"Created: {catchall_404}")
128199

129-
# Copy static files (like llms.txt) that can't be redirected via HTML
130-
static_files = ["llms.txt"]
131-
for static_file in static_files:
132-
src = script_dir / static_file
200+
# llms.txt can't be redirected via HTML, so publish the docs site's own
201+
# generated index. The committed copy is only a fallback.
202+
llms_txt = fetch_canonical_llms_txt()
203+
if llms_txt is not None:
204+
(output_dir / "llms.txt").write_text(llms_txt)
205+
print(f"Fetched: {output_dir / 'llms.txt'} (from {CANONICAL_LLMS_URL})")
206+
else:
207+
src = script_dir / "llms.txt"
133208
if src.exists():
134-
dst = output_dir / static_file
135-
dst.write_text(src.read_text())
136-
print(f"Copied: {dst}")
209+
(output_dir / "llms.txt").write_text(src.read_text())
210+
print(f"Copied: {output_dir / 'llms.txt'} (fallback, may be stale)")
211+
else:
212+
print("No llms.txt fetched and no committed fallback; skipping")
137213

138214
print(f"\nGenerated {len(redirects)} redirect files in {output_dir}")
139215

‎docs/llms.txt‎

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,49 @@
1-
# LangGraph
2-
3-
LangGraph documentation has moved to docs.langchain.com.
4-
5-
## Overview
6-
7-
- [LangGraph Overview](https://docs.langchain.com/oss/python/langgraph/overview): Introduction to LangGraph, a library for building stateful, multi-actor applications with LLMs.
8-
- [Why LangGraph?](https://docs.langchain.com/oss/python/langgraph/why-langgraph): Motivation for LangGraph and its key features.
9-
10-
## Core Concepts
11-
12-
- [Graph API](https://docs.langchain.com/oss/python/langgraph/graph-api): Learn how to define state, create nodes, and connect them with edges.
13-
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming): Stream outputs from your graph for better UX.
14-
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence): Add memory and checkpointing to your graphs.
15-
- [Add Memory](https://docs.langchain.com/oss/python/langgraph/add-memory): Implement short-term and long-term memory.
16-
- [Workflows & Agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents): Build agents and workflows with LangGraph.
17-
18-
## How-To Guides
19-
20-
- [Use Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs): Compose graphs using subgraphs.
21-
- [Observability](https://docs.langchain.com/oss/python/langgraph/observability): Add tracing and debugging to your graphs.
22-
- [Common Errors](https://docs.langchain.com/oss/python/langgraph/common-errors): Troubleshoot common LangGraph errors.
23-
24-
## Tutorials
25-
26-
- [Agentic RAG](https://docs.langchain.com/oss/python/langgraph/agentic-rag): Build an agentic RAG system with LangGraph.
27-
- [SQL Agent](https://docs.langchain.com/oss/python/langgraph/sql-agent): Create a SQL agent with LangGraph.
28-
29-
## Reference
30-
31-
- [API Reference](https://reference.langchain.com/python/langgraph/): Complete API documentation for LangGraph.
32-
33-
## LangGraph Platform
34-
35-
For deploying LangGraph applications in production, see the [LangSmith documentation](https://docs.langchain.com/langsmith/agent-server).
1+
# Docs by LangChain: LangGraph (Python)
2+
3+
> Markdown index of the LangGraph (Python) documentation.
4+
5+
## LangGraph (Python)
6+
7+
- [Memory](https://docs.langchain.com/oss/python/langgraph/add-memory.md)
8+
- [Build a custom RAG agent with LangGraph](https://docs.langchain.com/oss/python/langgraph/agentic-rag.md)
9+
- [Application structure](https://docs.langchain.com/oss/python/langgraph/application-structure.md)
10+
- [Backward compatibility](https://docs.langchain.com/oss/python/langgraph/backward-compatibility.md)
11+
- [Case studies](https://docs.langchain.com/oss/python/langgraph/case-studies.md)
12+
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-js.md)
13+
- [Changelog](https://docs.langchain.com/oss/python/langgraph/changelog-py.md)
14+
- [Checkpointers](https://docs.langchain.com/oss/python/langgraph/checkpointers.md)
15+
- [Choosing between Graph and Functional APIs](https://docs.langchain.com/oss/python/langgraph/choosing-apis.md)
16+
- [Deployment](https://docs.langchain.com/oss/python/langgraph/deploy.md)
17+
- [GRAPH_RECURSION_LIMIT](https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT.md)
18+
- [INVALID_CHAT_HISTORY](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CHAT_HISTORY.md)
19+
- [INVALID_CONCURRENT_GRAPH_UPDATE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_CONCURRENT_GRAPH_UPDATE.md)
20+
- [INVALID_GRAPH_NODE_RETURN_VALUE](https://docs.langchain.com/oss/python/langgraph/errors/INVALID_GRAPH_NODE_RETURN_VALUE.md)
21+
- [MISSING_CHECKPOINTER](https://docs.langchain.com/oss/python/langgraph/errors/MISSING_CHECKPOINTER.md)
22+
- [MULTIPLE_SUBGRAPHS](https://docs.langchain.com/oss/python/langgraph/errors/MULTIPLE_SUBGRAPHS.md)
23+
- [Event streaming](https://docs.langchain.com/oss/python/langgraph/event-streaming.md)
24+
- [Fault tolerance](https://docs.langchain.com/oss/python/langgraph/fault-tolerance.md)
25+
- [Custom stream channels](https://docs.langchain.com/oss/python/langgraph/frontend/custom-stream-channels.md)
26+
- [Graph execution](https://docs.langchain.com/oss/python/langgraph/frontend/graph-execution.md)
27+
- [Overview](https://docs.langchain.com/oss/python/langgraph/frontend/overview.md)
28+
- [Functional API overview](https://docs.langchain.com/oss/python/langgraph/functional-api.md)
29+
- [Graph API overview](https://docs.langchain.com/oss/python/langgraph/graph-api.md)
30+
- [Install LangGraph](https://docs.langchain.com/oss/python/langgraph/install.md)
31+
- [Interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts.md)
32+
- [Run a local server](https://docs.langchain.com/oss/python/langgraph/local-server.md)
33+
- [LangSmith Observability](https://docs.langchain.com/oss/python/langgraph/observability.md)
34+
- [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview.md)
35+
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence.md)
36+
- [LangGraph runtime](https://docs.langchain.com/oss/python/langgraph/pregel.md)
37+
- [Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart.md)
38+
- [Build a custom SQL agent](https://docs.langchain.com/oss/python/langgraph/sql-agent.md)
39+
- [Stores](https://docs.langchain.com/oss/python/langgraph/stores.md)
40+
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming.md)
41+
- [LangSmith Studio](https://docs.langchain.com/oss/python/langgraph/studio.md)
42+
- [Test](https://docs.langchain.com/oss/python/langgraph/test.md)
43+
- [Thinking in LangGraph](https://docs.langchain.com/oss/python/langgraph/thinking-in-langgraph.md)
44+
- [Agent Chat UI](https://docs.langchain.com/oss/python/langgraph/ui.md)
45+
- [Use the functional API](https://docs.langchain.com/oss/python/langgraph/use-functional-api.md)
46+
- [Use the graph API](https://docs.langchain.com/oss/python/langgraph/use-graph-api.md)
47+
- [Subgraphs](https://docs.langchain.com/oss/python/langgraph/use-subgraphs.md)
48+
- [Use time-travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel.md)
49+
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents.md)

0 commit comments

Comments
 (0)