From 729968099508600e715345dbb2ec9b02df4816e0 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 17 Jul 2026 21:19:08 +0200 Subject: [PATCH 1/2] Add agents copy button on docs --- docs/app/rxconfig.py | 2 + docs/app/tests/test_agent_files.py | 79 +++++++++++++++++++ docs/app/uv.lock | 88 +++++++++++----------- docs/app/xy_docs/breadcrumb.py | 10 +++ docs/app/xy_docs/constants.py | 4 + docs/app/xy_docs/plugins.py | 117 +++++++++++++++++++++++++++++ 6 files changed, 256 insertions(+), 44 deletions(-) create mode 100644 docs/app/tests/test_agent_files.py create mode 100644 docs/app/xy_docs/plugins.py diff --git a/docs/app/rxconfig.py b/docs/app/rxconfig.py index 4551529..96153da 100644 --- a/docs/app/rxconfig.py +++ b/docs/app/rxconfig.py @@ -4,6 +4,7 @@ import reflex_xy from reflex_site_shared.plugins import SharedSiteStylesPlugin from xy_docs.config import DOCS_CONFIG +from xy_docs.plugins import XYDocsAgentFilesPlugin from xy_docs.prerender import XyDocsMarkdownPlugin config = rx.Config( @@ -23,6 +24,7 @@ rx.plugins.RadixThemesPlugin(), rx.plugins.SitemapPlugin(trailing_slash="always"), XyDocsMarkdownPlugin(docs=DOCS_CONFIG), + XYDocsAgentFilesPlugin(docs=DOCS_CONFIG), reflex_xy.XYPlugin(), ], ) diff --git a/docs/app/tests/test_agent_files.py b/docs/app/tests/test_agent_files.py new file mode 100644 index 0000000..22f8407 --- /dev/null +++ b/docs/app/tests/test_agent_files.py @@ -0,0 +1,79 @@ +"""Tests for the agent-readable documentation files and page actions.""" + +from reflex_site_shared.docs.content import discover_docs +from rxconfig import config +from xy_docs.breadcrumb import xy_docs_breadcrumb +from xy_docs.config import DOCS_CONFIG +from xy_docs.constants import LLMS_FULL_TXT_PATH, PUBLIC_DOCS_URL +from xy_docs.plugins import ( + XYDocsAgentFilesPlugin, + build_llms_full_txt, + build_llms_txt, + markdown_asset_path, +) +from xy_docs.prerender import XyDocsMarkdownPlugin +from xy_docs.sidebar import xy_docs_sidebar + + +def _headings(content: str, level: int) -> list[str]: + """Collect headings of one level, ignoring fenced code blocks.""" + prefix = "#" * level + " " + headings: list[str] = [] + in_fence = False + for line in content.splitlines(): + if line.lstrip().startswith(("```", "~~~")): + in_fence = not in_fence + elif not in_fence and line.startswith(prefix): + headings.append(line) + return headings + + +def test_markdown_asset_paths_match_published_assets() -> None: + """Every derived Markdown path is actually published for its page.""" + published = { + path.as_posix() + for path, _content in XyDocsMarkdownPlugin(docs=DOCS_CONFIG).get_static_assets() + } + for page in discover_docs(DOCS_CONFIG): + asset = markdown_asset_path(page) + assert any(path.endswith(asset) for path in published), page.route + + +def test_llms_txt_indexes_every_page_under_the_public_url() -> None: + """The index links llms-full.txt and each page's public Markdown URL.""" + content = build_llms_txt(DOCS_CONFIG) + assert f"({PUBLIC_DOCS_URL}{LLMS_FULL_TXT_PATH})" in content + for page in discover_docs(DOCS_CONFIG): + assert f"({PUBLIC_DOCS_URL}/{markdown_asset_path(page)})" in content + + +def test_llms_full_txt_keeps_section_headers_above_page_content() -> None: + """The combined file has one H1 and an H2 section per page.""" + content = build_llms_full_txt(DOCS_CONFIG) + assert _headings(content, level=1) == ["# XY Documentation"] + section_headers = _headings(content, level=2) + for page in discover_docs(DOCS_CONFIG): + assert f"## {page.title}" in section_headers + + +def test_agent_files_publish_under_the_frontend_path() -> None: + """Both agent files land inside the configured frontend path.""" + assets = XYDocsAgentFilesPlugin(docs=DOCS_CONFIG).get_static_assets() + names = {path.name: path.as_posix() for path, _content in assets} + assert set(names) == {"llms.txt", "llms-full.txt"} + prefix = config.frontend_path.strip("/") + for path in names.values(): + assert f"/{prefix}/" in path + + +def test_breadcrumb_actions_use_public_and_local_urls() -> None: + """Page actions point at this site's Markdown, not root-relative paths.""" + for page in discover_docs(DOCS_CONFIG): + if page.route.strip("/"): + break + rendered = str(xy_docs_breadcrumb(page, xy_docs_sidebar(page.route))) + asset = markdown_asset_path(page) + public_host_and_path = PUBLIC_DOCS_URL.removeprefix("https://") + assert f"{public_host_and_path}/{asset}" in rendered + assert f"{PUBLIC_DOCS_URL}{LLMS_FULL_TXT_PATH}" in rendered + assert f"{config.frontend_path}/{asset}" in rendered diff --git a/docs/app/uv.lock b/docs/app/uv.lock index ac19195..e01db86 100644 --- a/docs/app/uv.lock +++ b/docs/app/uv.lock @@ -177,11 +177,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.30.2" +version = "3.31.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/f7/2165ef325da22d854b8f81ca4799395f2eb6afa55cdb52c7710f028b5336/filelock-3.30.2.tar.gz", hash = "sha256:1ea7c857465c897a4a6e64c1aace28ff6b83f5bc66c1c06ea148efa65bc2ec5d", size = 176823, upload-time = "2026-07-16T19:50:42.724Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/9f/994e80905542b748eb5b9f36d71458f0aea51a7be0fcb52ad959787dc1b7/filelock-3.31.0.tar.gz", hash = "sha256:c188cbc4307c18894c5424fa73f97ea7fa127ddf62192487546da3a214d0a381", size = 180931, upload-time = "2026-07-18T05:53:29.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/df/05118016cad66cd0d7c9417b2d4fc245be35decc4c36810f3c8dbf729d88/filelock-3.30.2-py3-none-any.whl", hash = "sha256:a64b58f75048ec39589983e97f5117163f822261dcb6ba843e098f05aac9663f", size = 94092, upload-time = "2026-07-16T19:50:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4a/e213905d3b8ad3d35d14fc056b36134a274e7f6a1050e94428b5be10a94c/filelock-3.31.0-py3-none-any.whl", hash = "sha256:739b73e580fe88bb78d830aeddbc492519ece3d97ac8368de13a2032c61010c1", size = 96080, upload-time = "2026-07-18T05:53:27.732Z" }, ] [[package]] @@ -717,11 +717,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, ] [[package]] @@ -1094,8 +1094,8 @@ wheels = [ [[package]] name = "reflex" -version = "0.9.7.post4.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.7.post5.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "click" }, { name = "granian", extra = ["reload"] }, @@ -1127,8 +1127,8 @@ dependencies = [ [[package]] name = "reflex-base" -version = "0.9.7.post4.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-base&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.7.post5.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-base&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, @@ -1139,8 +1139,8 @@ dependencies = [ [[package]] name = "reflex-components-code" -version = "0.9.2.post200.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-code&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.2.post201.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-code&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1150,8 +1150,8 @@ dependencies = [ [[package]] name = "reflex-components-core" -version = "0.9.7.post3.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-core&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.7.post4.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-core&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "python-multipart" }, { name = "reflex-base" }, @@ -1163,8 +1163,8 @@ dependencies = [ [[package]] name = "reflex-components-dataeditor" -version = "0.9.1.post240.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-dataeditor&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.1.post241.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-dataeditor&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1172,32 +1172,32 @@ dependencies = [ [[package]] name = "reflex-components-gridjs" -version = "0.9.1.post240.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-gridjs&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.1.post241.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-gridjs&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-internal" -version = "0.0.23.post38.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-internal&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.0.23.post39.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-internal&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex" }, ] [[package]] name = "reflex-components-lucide" -version = "1.0.2.post83.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-lucide&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "1.0.2.post84.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-lucide&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-markdown" -version = "0.9.3.post108.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-markdown&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.3.post109.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-markdown&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-code" }, @@ -1207,16 +1207,16 @@ dependencies = [ [[package]] name = "reflex-components-moment" -version = "0.9.2.post49.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-moment&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.2.post50.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-moment&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-plotly" -version = "0.9.3.post49.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-plotly&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.3.post50.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-plotly&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1224,8 +1224,8 @@ dependencies = [ [[package]] name = "reflex-components-radix" -version = "0.9.6.post3.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-radix&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.6.post4.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-radix&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1234,8 +1234,8 @@ dependencies = [ [[package]] name = "reflex-components-react-player" -version = "0.9.1.post240.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-react-player&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.1.post241.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-react-player&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-core" }, @@ -1243,16 +1243,16 @@ dependencies = [ [[package]] name = "reflex-components-recharts" -version = "0.9.1.post240.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-recharts&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.1.post241.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-recharts&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, ] [[package]] name = "reflex-components-sonner" -version = "0.9.1.post240.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-sonner&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.1.post241.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-components-sonner&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "reflex-base" }, { name = "reflex-components-lucide" }, @@ -1260,8 +1260,8 @@ dependencies = [ [[package]] name = "reflex-docgen" -version = "0.9.3.post3.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.9.3.post4.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-docgen&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "griffelib" }, { name = "mistletoe" }, @@ -1272,8 +1272,8 @@ dependencies = [ [[package]] name = "reflex-hosting-cli" -version = "0.1.67.post67.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-hosting-cli&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.1.67.post68.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-hosting-cli&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "click" }, { name = "httpx" }, @@ -1284,13 +1284,13 @@ dependencies = [ [[package]] name = "reflex-integrations-docs" -version = "0.0.0.post3246.dev0+2edaf128" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Fintegrations-docs&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.0.0.post3247.dev0+f79d011d" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Fintegrations-docs&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } [[package]] name = "reflex-site-shared" -version = "0.0.37" -source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=main#2edaf12869894e146b92f32e5de2d1816a2f3512" } +version = "0.0.38" +source = { git = "https://github.com/reflex-dev/reflex?subdirectory=packages%2Freflex-site-shared&rev=main#f79d011d07050cfedabff5cc8b3670363617a4d8" } dependencies = [ { name = "email-validator" }, { name = "httpx" }, diff --git a/docs/app/xy_docs/breadcrumb.py b/docs/app/xy_docs/breadcrumb.py index a45999f..61e31e8 100644 --- a/docs/app/xy_docs/breadcrumb.py +++ b/docs/app/xy_docs/breadcrumb.py @@ -2,9 +2,14 @@ import reflex as rx import reflex_components_internal as ui +from reflex_base.config import get_config +from reflex_site_shared.docs import docs_page_actions from reflex_site_shared.docs.models import DocsPage from reflex_site_shared.views.sidebar import docs_sidebar_drawer +from xy_docs.constants import LLMS_FULL_TXT_PATH, PUBLIC_DOCS_URL +from xy_docs.plugins import markdown_asset_path + _BREADCRUMB_LABELS = { "charts": "Chart Gallery", "gallery": "Chart Gallery", @@ -114,6 +119,11 @@ def xy_docs_breadcrumb(page: DocsPage, sidebar: rx.Component) -> rx.Component: class_name="flex flex-row items-center gap-[5px] overflow-hidden lg:gap-4", ), rx.box( + docs_page_actions( + markdown_url=f"{PUBLIC_DOCS_URL}/{markdown_asset_path(page)}", + llms_full_txt_url=f"{PUBLIC_DOCS_URL}{LLMS_FULL_TXT_PATH}", + copy_url=f"{get_config().frontend_path}/{markdown_asset_path(page)}", + ), ui.icon( "ArrowDown01Icon", size=14, diff --git a/docs/app/xy_docs/constants.py b/docs/app/xy_docs/constants.py index fdb208f..3791cf7 100644 --- a/docs/app/xy_docs/constants.py +++ b/docs/app/xy_docs/constants.py @@ -6,6 +6,8 @@ PUBLIC_XY_VERSION = os.getenv("XY_DOCS_PUBLIC_VERSION", "0.0.1").strip() DOCS_CHANNEL = os.getenv("XY_DOCS_CHANNEL", "preview").strip().lower() SOCIAL_IMAGE_URL = f"{PUBLIC_DOCS_URL}/xy-social-card.png" +LLMS_TXT_PATH = "/llms.txt" +LLMS_FULL_TXT_PATH = "/llms-full.txt" if DOCS_CHANNEL not in {"preview", "stable"}: msg = "XY_DOCS_CHANNEL must be either 'preview' or 'stable'" @@ -13,6 +15,8 @@ __all__ = [ "DOCS_CHANNEL", + "LLMS_FULL_TXT_PATH", + "LLMS_TXT_PATH", "PUBLIC_DOCS_URL", "PUBLIC_XY_VERSION", "SOCIAL_IMAGE_URL", diff --git a/docs/app/xy_docs/plugins.py b/docs/app/xy_docs/plugins.py new file mode 100644 index 0000000..9f08916 --- /dev/null +++ b/docs/app/xy_docs/plugins.py @@ -0,0 +1,117 @@ +"""XY-specific compiler assets for agent-readable documentation.""" + +from __future__ import annotations + +import dataclasses +from pathlib import Path +from typing import Any + +from reflex.constants import Dirs +from reflex_base.config import get_config +from reflex_base.plugins import Plugin +from reflex_site_shared.docs import DocsPage, DocsSiteConfig, discover_docs + +from xy_docs.constants import LLMS_FULL_TXT_PATH, LLMS_TXT_PATH, PUBLIC_DOCS_URL + + +def _public_url(path: str) -> str: + """Return an absolute URL for an XY docs asset path.""" + return f"{PUBLIC_DOCS_URL}{path}" + + +def markdown_asset_path(page: DocsPage) -> str: + """Return the route-relative Markdown asset path for a discovered page.""" + route = page.route.strip("/") + if page.relative_path.stem.lower() == "index": + return f"{route}/index.md" if route else "index.md" + return f"{route}.md" + + +def _page_markdown_url(page: DocsPage) -> str: + """Return the direct public Markdown asset URL for a discovered page.""" + return f"{PUBLIC_DOCS_URL}/{markdown_asset_path(page)}" + + +def _page_body(content: str) -> str: + """Drop a page's leading H1 so section headers stay the top heading level.""" + first_line, separator, rest = content.lstrip("\n").partition("\n") + if first_line.startswith("# "): + return rest.lstrip("\n") if separator else "" + return content + + +def build_llms_txt(config: DocsSiteConfig) -> str: + """Build the concise agent-readable index of public XY pages.""" + lines = [ + "# XY Documentation", + "", + ( + "> XY is a high-performance plotting library for Python and Reflex. " + "Use this index to find agent-readable Markdown docs, or see " + f"[llms-full.txt]({_public_url(LLMS_FULL_TXT_PATH)}) for the " + "complete docs in one file." + ), + "", + "## Docs", + "", + ] + for page in discover_docs(config): + description = f": {page.description}" if page.description else "" + lines.append(f"- [{page.title}]({_page_markdown_url(page)}){description}") + return "\n".join(lines).rstrip() + "\n" + + +def build_llms_full_txt(config: DocsSiteConfig) -> str: + """Combine all public XY pages into one agent-readable Markdown file.""" + lines = [ + "# XY Documentation", + f"Source: {PUBLIC_DOCS_URL}/", + "", + ( + "This file stitches together the full XY documentation as Markdown " + "for AI agents and LLM indexing." + ), + "", + ( + "For a navigable index with links to individual docs pages, see " + f"[llms.txt]({_public_url(LLMS_TXT_PATH)})." + ), + "", + ] + for page in discover_docs(config): + lines.extend( + ( + f"## {page.title}", + "", + f"Source: {_page_markdown_url(page)}", + "", + _page_body(page.content), + "", + ) + ) + return "\n".join(lines).rstrip() + "\n" + + +@dataclasses.dataclass(frozen=True, slots=True) +class XYDocsAgentFilesPlugin(Plugin): + """Publish concise and complete agent-readable XY documentation.""" + + docs: DocsSiteConfig + + def get_static_assets(self, **context: Any) -> tuple[tuple[Path, str], ...]: + """Emit ``llms.txt`` and ``llms-full.txt`` under the frontend path.""" + root = Path(Dirs.PUBLIC) + if frontend_path := get_config().frontend_path: + root /= frontend_path.lstrip("/") + return ( + (root / "llms.txt", build_llms_txt(self.docs)), + (root / "llms-full.txt", build_llms_full_txt(self.docs)), + ) + + +__all__ = [ + "XYDocsAgentFilesPlugin", + "build_llms_full_txt", + "build_llms_txt", + "markdown_asset_path", +] From 8d13283103c420b78f78eade7b4716638b8945d7 Mon Sep 17 00:00:00 2001 From: Alek Date: Sun, 19 Jul 2026 17:13:31 -0700 Subject: [PATCH 2/2] Fix agent asset test isolation --- docs/app/tests/test_agent_files.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/app/tests/test_agent_files.py b/docs/app/tests/test_agent_files.py index 22f8407..6e2acbb 100644 --- a/docs/app/tests/test_agent_files.py +++ b/docs/app/tests/test_agent_files.py @@ -1,5 +1,8 @@ """Tests for the agent-readable documentation files and page actions.""" +from pathlib import Path + +import pytest from reflex_site_shared.docs.content import discover_docs from rxconfig import config from xy_docs.breadcrumb import xy_docs_breadcrumb @@ -56,14 +59,15 @@ def test_llms_full_txt_keeps_section_headers_above_page_content() -> None: assert f"## {page.title}" in section_headers -def test_agent_files_publish_under_the_frontend_path() -> None: +def test_agent_files_publish_under_the_frontend_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: """Both agent files land inside the configured frontend path.""" + monkeypatch.setattr("xy_docs.plugins.get_config", lambda: config) assets = XYDocsAgentFilesPlugin(docs=DOCS_CONFIG).get_static_assets() - names = {path.name: path.as_posix() for path, _content in assets} - assert set(names) == {"llms.txt", "llms-full.txt"} - prefix = config.frontend_path.strip("/") - for path in names.values(): - assert f"/{prefix}/" in path + paths = {path for path, _content in assets} + root = Path("public") / config.frontend_path.strip("/") + assert paths == {root / "llms.txt", root / "llms-full.txt"} def test_breadcrumb_actions_use_public_and_local_urls() -> None: