Skip to content

Commit e772d75

Browse files
MilanMilan
authored andcommitted
Add auto-generated llms.txt for AI agent discoverability
1 parent eec26ff commit e772d75

4 files changed

Lines changed: 4649 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite dev",
8-
"build": "vite build",
8+
"build": "python3 scripts/build-llms-txt.py && vite build",
99
"preview": "vite preview",
1010
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1111
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",

scripts/build-llms-txt.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Generate llms.txt and llms-full.txt for the PathSim documentation site.
4+
5+
These files make the documentation discoverable by AI agents.
6+
7+
Usage:
8+
python scripts/build-llms-txt.py
9+
"""
10+
11+
import json
12+
import sys
13+
from pathlib import Path
14+
15+
sys.path.insert(0, str(Path(__file__).parent))
16+
17+
from lib.config import PACKAGES, STATIC_DIR, CATEGORIES
18+
19+
BASE_URL = "https://docs.pathsim.org"
20+
21+
DESCRIPTION = (
22+
"PathSim is a Python framework for simulating dynamical systems using "
23+
"block diagrams. It supports continuous-time, discrete-time, and hybrid "
24+
"systems with 18+ numerical solvers, hierarchical subsystems, event "
25+
"handling, and MIMO connections."
26+
)
27+
28+
29+
def load_package_manifest(package_id: str) -> dict | None:
30+
path = STATIC_DIR / package_id / "manifest.json"
31+
if not path.exists():
32+
return None
33+
with open(path, "r", encoding="utf-8") as f:
34+
return json.load(f)
35+
36+
37+
def load_version_data(package_id: str, tag: str) -> tuple[dict | None, dict | None]:
38+
version_dir = STATIC_DIR / package_id / tag
39+
api_data = None
40+
manifest = None
41+
42+
api_path = version_dir / "api.json"
43+
if api_path.exists():
44+
with open(api_path, "r", encoding="utf-8") as f:
45+
api_data = json.load(f)
46+
47+
manifest_path = version_dir / "manifest.json"
48+
if manifest_path.exists():
49+
with open(manifest_path, "r", encoding="utf-8") as f:
50+
manifest = json.load(f)
51+
52+
return api_data, manifest
53+
54+
55+
def generate_llms_txt() -> str:
56+
"""Generate the lightweight llms.txt index."""
57+
lines = []
58+
lines.append("# PathSim Documentation")
59+
lines.append("")
60+
lines.append(f"> {DESCRIPTION}")
61+
lines.append("")
62+
63+
for package_id, pkg_config in PACKAGES.items():
64+
pkg_manifest = load_package_manifest(package_id)
65+
if not pkg_manifest:
66+
continue
67+
68+
latest = pkg_manifest["latestTag"]
69+
display_name = pkg_config["display_name"]
70+
71+
api_data, manifest = load_version_data(package_id, latest)
72+
73+
lines.append(f"## {display_name} ({latest})")
74+
lines.append("")
75+
76+
# API overview
77+
if api_data:
78+
api_url = f"{BASE_URL}/{package_id}/{latest}/api"
79+
lines.append(f"- [{display_name} API Reference]({api_url}): Full API documentation")
80+
81+
modules = api_data.get("modules", {})
82+
for module_name, module in modules.items():
83+
desc = module.get("description", "")
84+
anchor = module_name.replace(".", "-")
85+
entry = f"- [{module_name}]({api_url}#{anchor})"
86+
if desc:
87+
entry += f": {desc}"
88+
lines.append(entry)
89+
90+
for cls in module.get("classes", []):
91+
cls_desc = cls.get("description", "")
92+
cls_entry = f" - [{cls['name']}]({api_url}#{cls['name']})"
93+
if cls_desc:
94+
cls_entry += f": {cls_desc}"
95+
lines.append(cls_entry)
96+
97+
lines.append("")
98+
99+
# Examples
100+
if manifest:
101+
notebooks = manifest.get("notebooks", [])
102+
if notebooks:
103+
lines.append(f"### Examples")
104+
lines.append("")
105+
for nb in notebooks:
106+
slug = nb.get("slug", "")
107+
title = nb.get("title", slug)
108+
desc = nb.get("description", "")
109+
url = f"{BASE_URL}/{package_id}/{latest}/examples/{slug}"
110+
entry = f"- [{title}]({url})"
111+
if desc:
112+
entry += f": {desc}"
113+
lines.append(entry)
114+
lines.append("")
115+
116+
# Links
117+
lines.append("## Links")
118+
lines.append("")
119+
lines.append("- [PathSim Homepage](https://pathsim.org): Project homepage")
120+
lines.append("- [PathView Editor](https://view.pathsim.org): Browser-based visual block diagram editor")
121+
lines.append("- [GitHub](https://github.com/pathsim): Source code repositories")
122+
lines.append("- [PyPI](https://pypi.org/project/pathsim): Python package")
123+
lines.append("- [JOSS Paper](https://doi.org/10.21105/joss.07484): Published paper")
124+
lines.append("")
125+
126+
return "\n".join(lines)
127+
128+
129+
def generate_llms_full_txt() -> str:
130+
"""Generate llms-full.txt with complete API documentation content."""
131+
lines = []
132+
lines.append("# PathSim Documentation (Full)")
133+
lines.append("")
134+
lines.append(f"> {DESCRIPTION}")
135+
lines.append("")
136+
137+
for package_id, pkg_config in PACKAGES.items():
138+
pkg_manifest = load_package_manifest(package_id)
139+
if not pkg_manifest:
140+
continue
141+
142+
latest = pkg_manifest["latestTag"]
143+
display_name = pkg_config["display_name"]
144+
145+
api_data, manifest = load_version_data(package_id, latest)
146+
147+
lines.append(f"## {display_name} ({latest})")
148+
lines.append("")
149+
150+
# Full API content
151+
if api_data:
152+
modules = api_data.get("modules", {})
153+
for module_name, module in modules.items():
154+
lines.append(f"### {module_name}")
155+
lines.append("")
156+
157+
desc = module.get("description", "")
158+
if desc:
159+
lines.append(desc)
160+
lines.append("")
161+
162+
for cls in module.get("classes", []):
163+
cls_name = cls["name"]
164+
cls_desc = cls.get("description", "")
165+
bases = cls.get("bases", [])
166+
167+
base_str = f"({', '.join(bases)})" if bases else ""
168+
lines.append(f"#### class {cls_name}{base_str}")
169+
lines.append("")
170+
171+
if cls_desc:
172+
lines.append(cls_desc)
173+
lines.append("")
174+
175+
# Attributes
176+
for attr in cls.get("attributes", []):
177+
attr_name = attr.get("name", "")
178+
attr_type = attr.get("type", "")
179+
attr_desc = attr.get("description", "")
180+
if attr_name.startswith("_"):
181+
continue
182+
type_str = f": {attr_type}" if attr_type else ""
183+
entry = f"- `{attr_name}{type_str}`"
184+
if attr_desc:
185+
entry += f" — {attr_desc}"
186+
lines.append(entry)
187+
188+
if cls.get("attributes"):
189+
lines.append("")
190+
191+
# Methods
192+
for method in cls.get("methods", []):
193+
method_name = method.get("name", "")
194+
if method_name.startswith("_") and method_name != "__init__":
195+
continue
196+
sig = method.get("signature", "()")
197+
method_desc = method.get("description", "")
198+
lines.append(f"**{cls_name}.{method_name}**`{sig}`")
199+
if method_desc:
200+
lines.append(f": {method_desc}")
201+
lines.append("")
202+
203+
for func in module.get("functions", []):
204+
func_name = func.get("name", "")
205+
sig = func.get("signature", "()")
206+
func_desc = func.get("description", "")
207+
lines.append(f"#### {func_name}`{sig}`")
208+
if func_desc:
209+
lines.append(func_desc)
210+
lines.append("")
211+
212+
# Examples with descriptions
213+
if manifest:
214+
notebooks = manifest.get("notebooks", [])
215+
if notebooks:
216+
lines.append(f"### Examples")
217+
lines.append("")
218+
for nb in notebooks:
219+
slug = nb.get("slug", "")
220+
title = nb.get("title", slug)
221+
desc = nb.get("description", "")
222+
tags = nb.get("tags", [])
223+
category = nb.get("category", "")
224+
url = f"{BASE_URL}/{package_id}/{latest}/examples/{slug}"
225+
226+
lines.append(f"#### [{title}]({url})")
227+
if desc:
228+
lines.append(desc)
229+
if tags:
230+
lines.append(f"Tags: {', '.join(tags)}")
231+
lines.append("")
232+
233+
# Links
234+
lines.append("## Links")
235+
lines.append("")
236+
lines.append("- [PathSim Homepage](https://pathsim.org): Project homepage")
237+
lines.append("- [PathView Editor](https://view.pathsim.org): Browser-based visual block diagram editor")
238+
lines.append("- [GitHub](https://github.com/pathsim): Source code repositories")
239+
lines.append("- [PyPI](https://pypi.org/project/pathsim): Python package")
240+
lines.append("- [JOSS Paper](https://doi.org/10.21105/joss.07484): Published paper")
241+
lines.append("")
242+
243+
return "\n".join(lines)
244+
245+
246+
def main():
247+
output_dir = STATIC_DIR
248+
249+
llms_txt = generate_llms_txt()
250+
llms_path = output_dir / "llms.txt"
251+
with open(llms_path, "w", encoding="utf-8") as f:
252+
f.write(llms_txt)
253+
print(f"Generated {llms_path} ({len(llms_txt)} bytes)")
254+
255+
llms_full_txt = generate_llms_full_txt()
256+
llms_full_path = output_dir / "llms-full.txt"
257+
with open(llms_full_path, "w", encoding="utf-8") as f:
258+
f.write(llms_full_txt)
259+
print(f"Generated {llms_full_path} ({len(llms_full_txt)} bytes)")
260+
261+
262+
if __name__ == "__main__":
263+
main()

0 commit comments

Comments
 (0)