Repository navigation
MOD-19391: Redis Agent Playbooks developer docs #4249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
tylerhutcherson
merged 18 commits into
iris-ga-docs
from
MOD-19391-playbook-developer-docs
Oct 22, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
85cd136
docs: scaffold Agent Playbooks guides and review handoff
tylerhutcherson 114112c
docs(iris): refine Playbooks developer guides and examples
tylerhutcherson 5ddff46
MOD-19391 Add Python, TypeScript, and cURL pseudo-clients for Iris ex…
andy-stark-redis eb745e9
docs: align Playbooks navigation with the Iris docs space
tylerhutcherson e2f9e31
docs: give the FAQ authoring guide a task-specific URL
tylerhutcherson 6c6a2b9
docs: simplify cross-language naming explanations
tylerhutcherson c442956
docs: use standard TCE sources for Playbooks examples
tylerhutcherson 67f9016
docs: separate Playbooks MCP and agent integration guides
tylerhutcherson 03da1f1
docs: simplify Playbooks MCP examples and guide navigation
tylerhutcherson f5a1e09
docs: improve Playbooks navigation and Python examples
tylerhutcherson b07e7ee
docs: add Playbooks workflow diagrams
tylerhutcherson 772939a
docs: clarify Redis Agent Playbooks terminology
tylerhutcherson 9996b99
docs: align Playbooks guides with alpha.3 SDKs
tylerhutcherson 05a9897
docs: add unified Python and CLI Playbooks examples
tylerhutcherson c51fa40
docs: focus Proposals guide on reusable agent knowledge
tylerhutcherson 12bd469
docs: show Iris CLI commands directly
tylerhutcherson 428b113
docs: simplify Playbooks examples and fix retention diagram
tylerhutcherson 7520513
docs: fix Playbooks quickstart navigation
tylerhutcherson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .generated/ | ||
| !package-lock.json |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| """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): | ||
| """Collect published shared steps and their retained TypeScript test fixtures.""" | ||
| result = [] | ||
| seen = set() | ||
| for attrs in calls(page): | ||
| if not {CLIENTS["py"], CLIENTS["sh"]}.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")) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CI ignores Playbooks diagram asset changes
Medium Severity
The Playbooks docs workflow path filters omit
static/images/ai/context-engine/.test_proposal_diagram_qualifies_default_retention_windowsreadsagent-playbook-proposals.svg, so a diagram-only edit that drops the default 30-day labels would not run that check.Additional Locations (1)
build/agent_playbook_docs/test_reference.py#L64-L71Reviewed by Cursor Bugbot for commit 7520513. Configure here.