Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ on:
branches: [main]
pull_request:
branches: [main]
types: [opened, reopened, synchronize, closed]
workflow_dispatch:

jobs:
docs:
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -33,3 +35,37 @@ jobs:
- name: Deploy to GitHub Pages
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
run: uv run mkdocs gh-deploy --force

preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
concurrency:
group: preview-${{ github.event.number }}
steps:
- uses: actions/checkout@v4
if: github.event.action != 'closed'

- uses: actions/setup-python@v5
if: github.event.action != 'closed'
with:
python-version: "3.12"

- name: Install uv
if: github.event.action != 'closed'
uses: astral-sh/setup-uv@v5

- name: Install docs dependencies
if: github.event.action != 'closed'
run: uv sync --group docs

- name: Build docs
if: github.event.action != 'closed'
run: uv run mkdocs build

- name: Deploy PR preview
uses: rossjrw/pr-preview-action@v1
with:
source-dir: ./site
146 changes: 127 additions & 19 deletions hooks/gen_use_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,34 @@
summary: One or two sentences shown in the overview table and page.
status: published # omit, or 'draft' to hide it
order: 10 # optional sort key (default 100)
guides: # optional walkthrough links
guides: # optional walkthrough doc(s)
- text: Walkthrough
path: demo.md # path within the use-case folder
---

It then appears in the overview table, gets its own docs page, and lands in
the "Use Cases" nav group — no edits to mkdocs.yml or the docs tree required.

Each ``guides[].path`` doc (often the README itself, sometimes a dedicated
walkthrough file) is inlined into the generated page rather than linked out to
GitHub: its own frontmatter and leading ``# Title`` line are stripped (the
generated page supplies its own), and every relative link/image is rewritten
— to another use case's generated page when it points at that use case's
folder or guide doc, otherwise to a GitHub blob/tree URL, since only ``docs/``
itself is served by the built site.

Demo-bundle links (a downloadable ``.tar.gz`` snapshot of the use case's
folder, hosted on Google Drive) come from ``use_cases/links.csv`` (columns:
folder name, URL) keyed by folder name, and are rendered only on the
generated docs page (alongside the GitHub source link) — READMEs carry no
hardcoded copy, so there's nothing to keep in sync.
"""

from __future__ import annotations

import csv
import logging
import re
from pathlib import Path

import yaml
Expand All @@ -32,11 +48,25 @@
TABLE_MARKER = "<!-- USE_CASES_TABLE -->"
INDEX_URI = "use-cases/index.md"

_FENCE_RE = re.compile(r"^(```|~~~)")
_H1_RE = re.compile(r"^#\s")
_LINK_RE = re.compile(r"(!?\[[^\]]*\]\()([^()\s]+)(\))")

# Populated in on_config, consumed in on_files / on_page_markdown within the
# same build. Module-level is fine: each build re-runs on_config first.
_use_cases: list[dict] = []


def _load_demo_urls(uc_dir: Path) -> dict[str, str]:
csv_path = uc_dir / "links.csv"
if not csv_path.is_file():
return {}
with csv_path.open(encoding="utf-8", newline="") as f:
return {
name.strip(): url.strip() for name, url in csv.reader(f) if name.strip()
}


def _parse_frontmatter(text: str) -> dict | None:
if not text.startswith("---"):
return None
Expand All @@ -50,11 +80,86 @@ def _parse_frontmatter(text: str) -> dict | None:
return data if isinstance(data, dict) else None


def _strip_frontmatter(text: str) -> str:
if not text.startswith("---"):
return text
parts = text.split("---", 2)
return parts[2].lstrip("\n") if len(parts) >= 3 else text


def _strip_leading_h1(text: str) -> str:
lines = text.splitlines()
i = 0
while i < len(lines) and not lines[i].strip():
i += 1
if i >= len(lines) or not _H1_RE.match(lines[i]):
return text # no leading title line — leave the body as-is
i += 1
while i < len(lines) and not lines[i].strip():
i += 1
return "\n".join(lines[i:])


def _rewrite_link_target(
target: str,
uc: dict,
gh: str,
uc_names: set[str],
page_by_path: dict[Path, str],
) -> str:
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*:", target) or target.startswith("#"):
return target # absolute URL (incl. mailto:) or a same-page anchor
path, sep, fragment = target.partition("#")
repo_root = uc["dir"].parent.parent
resolved = (uc["dir"] / path).resolve()
try:
rel = resolved.relative_to(repo_root)
except ValueError:
return target # escapes the repo entirely — leave it alone
if resolved in page_by_path:
return f"{page_by_path[resolved]}.md" # links at another use case's guide page
use_cases_dir = uc["dir"].parent
if resolved.parent == use_cases_dir and resolved.name in uc_names:
return f"{resolved.name}.md" # links straight at a sibling use case's folder
kind = "tree" if resolved.is_dir() else "blob"
return f"{gh}/{kind}/main/{rel.as_posix()}" + (sep + fragment if sep else "")


def _inline_readme(
uc: dict,
path: str,
gh: str,
uc_names: set[str],
page_by_path: dict[Path, str],
) -> str:
text = (uc["dir"] / path).read_text(encoding="utf-8")
text = _strip_leading_h1(_strip_frontmatter(text))
in_fence = False
lines = []
for line in text.splitlines():
if _FENCE_RE.match(line.strip()):
in_fence = not in_fence
lines.append(line)
continue
if in_fence:
lines.append(line)
continue
line = _LINK_RE.sub(
lambda m: m[1]
+ _rewrite_link_target(m[2], uc, gh, uc_names, page_by_path)
+ m[3],
line,
)
lines.append(line)
return "\n".join(lines)


def _discover(config) -> list[dict]:
uc_dir = Path(config.docs_dir).parent / "use_cases"
found: list[dict] = []
if not uc_dir.is_dir():
return found
demo_urls = _load_demo_urls(uc_dir)
for folder in sorted(p for p in uc_dir.iterdir() if p.is_dir()):
readme = folder / "README.md"
if not readme.is_file():
Expand All @@ -80,6 +185,8 @@ def _discover(config) -> list[dict]:
"summary": " ".join(str(fm["summary"]).split()),
"order": fm.get("order", 100),
"guides": fm.get("guides") or [],
"demo_url": demo_urls.get(folder.name),
"dir": folder.resolve(),
}
)
found.sort(key=lambda u: (u["order"], u["title"].lower()))
Expand All @@ -95,17 +202,17 @@ def on_config(config):
for item in config.nav or []:
if isinstance(item, dict) and isinstance(item.get("Use Cases"), list):
children = item["Use Cases"]
present = {
next(iter(c.values())) for c in children if isinstance(c, dict)
}
present = {next(iter(c.values())) for c in children if isinstance(c, dict)}
for uc in _use_cases:
uri = f"use-cases/{uc['name']}.md"
if uri not in present:
children.append({uc["title"]: uri})
return config


def _render_page(uc: dict, repo_url: str) -> str:
def _render_page(
uc: dict, repo_url: str, uc_names: set[str], page_by_path: dict[Path, str]
) -> str:
gh = (repo_url or "").rstrip("/")
out = [f"# {uc['title']}", "", f"**Domain:** {uc['domain']}", ""]
if gh:
Expand All @@ -114,30 +221,31 @@ def _render_page(uc: dict, repo_url: str) -> str:
f"({gh}/tree/main/use_cases/{uc['name']}/)",
"",
]
if uc["demo_url"]:
out += [f"**Demo bundle:** [Download `.tar.gz`]({uc['demo_url']})", ""]
out += [uc["summary"], ""]
guides = [g for g in uc["guides"] if isinstance(g, dict)]
if guides:
out += ["## Guides", ""]
for g in guides:
path = g.get("path", "")
text = g.get("text") or path
if gh and path:
out.append(
f"- [{text}]({gh}/blob/main/use_cases/{uc['name']}/{path})"
)
elif text:
out.append(f"- {text}")
out.append("")
for g in uc["guides"]:
if not isinstance(g, dict):
continue
path = g.get("path", "")
if path:
out += [_inline_readme(uc, path, gh, uc_names, page_by_path), ""]
return "\n".join(out)


def on_files(files, config):
uc_names = {uc["name"] for uc in _use_cases}
page_by_path: dict[Path, str] = {}
for uc in _use_cases:
for g in uc["guides"]:
if isinstance(g, dict) and g.get("path"):
page_by_path[(uc["dir"] / g["path"]).resolve()] = uc["name"]
for uc in _use_cases:
files.append(
File.generated(
config,
f"use-cases/{uc['name']}.md",
content=_render_page(uc, config.repo_url),
content=_render_page(uc, config.repo_url, uc_names, page_by_path),
)
)
return files
Expand Down
15 changes: 15 additions & 0 deletions use_cases/isaac_skills_demo/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
---
title: Skill-Driven VASP → ISAAC Conversion
domain: Skill management — external skill catalog (K-Dense) authoring a VASP → ISAAC converter
summary: >-
A lightweight mock of the isaac_vasp workflow where the agent itself
discovers, syncs, installs, and authors skills (pymatgen, skill-creator) to
convert mock VASP output into an ISAAC record, vetting the skill-management
feature end-to-end.
status: published
order: 70
guides:
- text: Skill-Driven VASP → ISAAC Walkthrough
path: README.md
---

# DSAgt Demo: Skill-Driven VASP → ISAAC Conversion

> **Estimated time:** ~15 minutes — the agent flow runs in seconds on the
Expand Down
10 changes: 10 additions & 0 deletions use_cases/links.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
aidrin_full_tour,https://drive.google.com/uc?export=download&id=1dngd2AfWkM_8NMXfjE5T7hrXGmh9C4_9
aidrin_readiness_gate,https://drive.google.com/uc?export=download&id=1QxNup8WG0x1r7VNIB8NplnHrYX0ISv2J
comb-flow-uni,https://drive.google.com/uc?export=download&id=1g53VKzl5rzr0rJsE7KmZkngjNyi3EbuW
cryoem,https://drive.google.com/uc?export=download&id=1xNTL9vqSD4HnWMnpmgwihq-viU0Cus7V
fusion-fm,https://drive.google.com/uc?export=download&id=1qhlcl6Bou4JE2XPzlU3MdFmGlyeFHpHL
genesis_skills,https://drive.google.com/uc?export=download&id=1nji0Avc-n952isq5aKGLoZgTkYHe0jzR
isaac_skills_demo,https://drive.google.com/uc?export=download&id=19PNObF-FZkGITNJ_VIZH8j9BHWSqRPrH
isaac_vasp,https://drive.google.com/uc?export=download&id=1uH0r7ryF9nUJaE1fxXZMAzBjiE4TXxWu
microbial_isolates,https://drive.google.com/uc?export=download&id=1wzS-RwP89bAKDYFXiqssTg7knf6LLCUT
tokamak_stability,https://drive.google.com/uc?export=download&id=1Czzwec9go5JV7tCCli8YJcVOKJqirFgb
Loading