Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
79b0b08
docs: scaffold Agent Playbooks guides and review handoff
tylerhutcherson Oct 1, 2026
6cfd434
docs(iris): refine Playbooks developer guides and examples
tylerhutcherson Oct 12, 2026
afb06d0
MOD-19391 Add Python, TypeScript, and cURL pseudo-clients for Iris ex…
andy-stark-redis Oct 14, 2026
e9a9733
docs: align Playbooks navigation with the Iris docs space
tylerhutcherson Oct 14, 2026
678179f
docs: give the FAQ authoring guide a task-specific URL
tylerhutcherson Oct 14, 2026
cb7cfed
docs: simplify cross-language naming explanations
tylerhutcherson Oct 14, 2026
364beb2
docs: use standard TCE sources for Playbooks examples
tylerhutcherson Oct 14, 2026
7c162e8
docs: separate Playbooks MCP and agent integration guides
tylerhutcherson Oct 14, 2026
d7293c3
docs: simplify Playbooks MCP examples and guide navigation
tylerhutcherson Oct 14, 2026
252d8df
docs: improve Playbooks navigation and Python examples
tylerhutcherson Oct 15, 2026
a73af6b
docs: add Playbooks workflow diagrams
tylerhutcherson Oct 15, 2026
76f9293
docs: clarify Redis Agent Playbooks terminology
tylerhutcherson Oct 15, 2026
1009f90
docs: align Playbooks guides with alpha.3 SDKs
tylerhutcherson Oct 18, 2026
8a4411c
docs: add unified Python and CLI Playbooks examples
tylerhutcherson Oct 21, 2026
9a3a264
docs: focus Proposals guide on reusable agent knowledge
tylerhutcherson Oct 21, 2026
c45109e
docs: show Iris CLI commands directly
tylerhutcherson Oct 21, 2026
7c9291f
docs: add self-managed Agent Playbooks operator guides
tylerhutcherson Oct 13, 2026
53e3a78
docs: tighten Playbooks operator onboarding and handoffs
tylerhutcherson Oct 14, 2026
430c680
docs: align Playbooks operator guides with Iris main
tylerhutcherson Oct 18, 2026
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
41 changes: 41 additions & 0 deletions .github/workflows/check-playbook-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Check Playbooks documentation

on:
pull_request:
paths:
- 'content/develop/ai/context-engine/agent-playbook/**'
- 'build/agent_playbook_docs/**'
- 'local_examples/iris/playbooks/**'
- 'build/components/example.py'
- 'build/local_examples.py'
- 'build/example-test-harness/clients.tsv'
- 'config.toml'
- 'layouts/partials/process-markdown-content.html'
- 'layouts/_default/apireference.html'
- '.github/workflows/check-playbook-docs.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
examples:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: build/agent_playbook_docs/requirements.txt
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: build/agent_playbook_docs/package-lock.json
- run: python -m pip install -r build/agent_playbook_docs/requirements.txt
- run: npm ci --prefix build/agent_playbook_docs
- run: python build/agent_playbook_docs/check_sdk_source.py
- run: python build/agent_playbook_docs/examples.py build/agent_playbook_docs/.generated
- run: build/agent_playbook_docs/node_modules/.bin/tsc -p build/agent_playbook_docs
- run: python -m pytest build/agent_playbook_docs --rootdir . -q
2 changes: 2 additions & 0 deletions build/agent_playbook_docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.generated/
!package-lock.json
71 changes: 71 additions & 0 deletions build/agent_playbook_docs/check_sdk_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Check the docs API against the locked iris-sdks public contract.

Without arguments, verify the vendored contract without network access.
With --sdk-repo, also verify the local SDK contract, example overlay, and version.
An upstream change requires reviewing the source lock and SDK pin together.
"""

import argparse
from copy import deepcopy
import hashlib
import json
from pathlib import Path

import yaml


ROOT = Path(__file__).resolve().parents[2]
LOCK = Path(__file__).with_name("source-lock.json")
SPEC = ROOT / "content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json"


def wire_contract(document):
result = deepcopy(document)
result["info"].pop("title", None)
result.pop("tags", None)
for methods in result["paths"].values():
for operation in methods.values():
if not isinstance(operation, dict) or "operationId" not in operation:
continue
operation.pop("tags", None)
operation.pop("summary", None)
return result


def digest(document):
content = json.dumps(wire_contract(document), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(content.encode()).hexdigest()


def check(sdk_repo=None):
lock = json.loads(LOCK.read_text())
expected = lock["data_plane_contract_sha256"]
if digest(json.loads(SPEC.read_text())) != expected:
raise ValueError("Docs contract differs from the locked SDK input. Regenerate and review it.")
if sdk_repo is not None:
upstream = json.loads((sdk_repo / lock["sdk_contract_path"]).read_text())
if digest(upstream) != expected:
raise ValueError("SDK contract changed. Review the API, examples, version pin, and source lock together.")
overlay = sdk_repo / lock["sdk_example_overlay_path"]
if hashlib.sha256(overlay.read_bytes()).hexdigest() != lock["sdk_example_overlay_sha256"]:
raise ValueError("SDK usage examples changed. Review the docs walkthroughs before updating the source lock.")
config = yaml.safe_load((sdk_repo / "playbook-python-sdk/.speakeasy/gen.yaml").read_text())
if config["python"]["version"] != lock["sdk_generator_version"]:
raise ValueError("SDK version changed. Review the docs dependency pin.")
typescript = json.loads((sdk_repo / "playbook-ts-sdk/package.json").read_text())
if f"{typescript['name']}@{typescript['version']}" != lock["typescript_package"]:
raise ValueError("TypeScript SDK version changed. Review the docs dependency pin.")
return lock


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sdk-repo", type=Path)
args = parser.parse_args()
try:
lock = check(args.sdk_repo)
except ValueError as error:
parser.exit(1, str(error) + "\n")
print(f"Docs match the locked SDK contract ({lock['python_package']}).")
if args.sdk_repo:
print("Local SDK contract, usage overlay, and generator version also match.")
159 changes: 159 additions & 0 deletions build/agent_playbook_docs/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Read the standard TCE sources for Playbooks validation.

Site builds use build/local_examples.py and clients-example. This adapter exposes
those same processed source files and named steps to the API example checks.
"""

from functools import lru_cache
from pathlib import Path
import re
import shutil
import sys
from tempfile import TemporaryDirectory

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "build"))
from components.example import Example
from local_examples import get_example_id_from_file, get_language_from_extension

DOCS = ROOT / "content/develop/ai/context-engine/agent-playbook"
SOURCES = ROOT / "local_examples/iris/playbooks"
GUIDES = ("quickstart", "faqs", "skills", "proposals", "retrieval", "mcp", "limits")
AGENT_GUIDES = ("openai_agent", "langgraph_agent")
PAGE_NAMES = {"faqs": "manage-faqs", "skills": "manage-skills"}
CLIENTS = {}
for row in (ROOT / "build/example-test-harness/clients.tsv").read_text().splitlines():
if row.startswith("iris-") and not row.startswith("iris-cli\t"):
fields = row.split("\t")
CLIENTS[fields[6].rsplit(".", 1)[1]] = fields[1]
assert set(CLIENTS) == {"py", "mts", "sh"}
ALL_CLIENTS = {**CLIENTS, "cli.sh": "CLI (Iris)"}
CALLS = re.compile(r'\{\{< clients-example\s+([^>]*?)/>\}\}', re.S)
ATTRS = re.compile(r'(\w+)="([^"]*)"')
FENCE = re.compile(r"```(?:python|typescript|bash)\n(.*?)\n```", re.S)


def source_path(page, suffix, action=None):
name = f"{page}_{action}" if action else "mcp_client" if page == "mcp" else page
return SOURCES / page / f"{name}.{suffix}"


@lru_cache(maxsize=128)
def _processed(path, mtime):
with TemporaryDirectory(prefix="playbook-tce-") as directory:
target = Path(directory) / path.name
shutil.copy2(path, target)
example = Example(get_language_from_extension(path.name), str(target))
assert example.named_steps, f"No named steps in {path}"
return target.read_text(), example.named_steps


def processed(path):
return _processed(path, path.stat().st_mtime_ns)


def bundle(set_id):
result = {}
for path in SOURCES.rglob("*"):
suffix = "cli.sh" if path.name.endswith(".cli.sh") else path.suffix.lstrip(".")
if suffix in ALL_CLIENTS and get_example_id_from_file(str(path)) == set_id:
assert suffix not in result, f"Duplicate {suffix} source in {set_id}"
result[suffix] = path
assert result, f"Unknown Playbook example set: {set_id}"
return result


def step(path, name):
source, steps = processed(path)
assert name in steps, f"Missing {name} in {path}"
start, end = map(int, steps[name].split("-"))
return "\n".join(source.splitlines()[start - 1:end])


def calls(page):
source = (DOCS / f"{PAGE_NAMES.get(page, page)}.md").read_text()
return [dict(ATTRS.findall(body)) for body in CALLS.findall(source)]


def blocks(page):
result = []
seen = set()
for attrs in calls(page):
if not set(CLIENTS.values()).issubset(attrs.get("lang_filter", "").split(",")):
continue
name = attrs["step"]
assert name not in seen, f"Duplicate step in {page}: {name}"
seen.add(name)
files = bundle(attrs["set"])
result.append((name, [step(files[suffix], name) for suffix in CLIENTS]))
return result


def skill_package():
return step(source_path("skills", "sh"), "playbook-skill-package")


def scripts(page, omit=()):
selected = [block for block in blocks(page) if block[0] not in omit]
if page not in ("quickstart", "mcp"):
setup = next(block for block in blocks("quickstart") if block[0] == "playbook-connect")
selected.insert(0, setup)
result = ["\n\n".join(block[1][i] for block in selected) + "\n" for i in range(3)]
if page == "skills":
result[2] = skill_package() + "\n\n" + result[2]
result[2] = "set -euo pipefail\n\n" + result[2]
return dict(zip(CLIENTS, result))


def full_examples():
return {source_path(page, suffix): processed(source_path(page, suffix))[0]
for page in ("quickstart", "skills") for suffix in ALL_CLIENTS}


def cli_script(page):
selected = [attrs for attrs in calls(page)
if "CLI (Iris)" in attrs.get("lang_filter", "").split(",")
and not attrs["set"].endswith(("_delete", "_withdraw"))]
if not selected:
return None
pieces = []
if page != "quickstart":
pieces.append(step(source_path("quickstart", "cli.sh"), "playbook-connect"))
for attrs in selected:
pieces.append(step(bundle(attrs["set"])["cli.sh"], attrs["step"]))
if page == "skills":
pieces.insert(0, skill_package())
return "set -euo pipefail\n\n" + "\n\n".join(pieces) + "\n"


def agent_scripts():
return {(page, suffix): processed(source_path(page, suffix))[0]
for page in AGENT_GUIDES for suffix in ("py", "mts")}


def standalone_python(page, heading):
assert (page, heading) == ("quickstart", "Use Python asynchronously")
return processed(SOURCES / "async/async_example.py")[0]


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("output", type=Path)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
for path in args.output.glob("*_download.*"):
path.unlink()
for page in GUIDES:
for suffix, source in scripts(page).items():
(args.output / f"{page}_example.{suffix}").write_text(source)
if source := cli_script(page):
(args.output / f"{page}_example.cli.sh").write_text(source)
for path, source in full_examples().items():
suffix = "cli.sh" if path.name.endswith(".cli.sh") else path.suffix[1:]
name = path.name.removesuffix("." + suffix)
(args.output / f"{name}_copy.{suffix}").write_text(source)
for (page, suffix), source in agent_scripts().items():
(args.output / f"{page}_example.{suffix}").write_text(source)
(args.output / "async_search_example.py").write_text(standalone_python("quickstart", "Use Python asynchronously"))
Loading
Loading