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
11 changes: 4 additions & 7 deletions .github/workflows/check-playbook-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ on:
pull_request:
paths:
- 'content/develop/ai/context-engine/agent-playbooks/**'
- 'content/develop/ai/context-engine/sdk-and-cli.md'
- 'content/operate/iris/agent-playbook/**'
- 'static/images/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/partials/tabbed-clients-example.html'
- 'layouts/_default/apireference.html'
- '.github/workflows/check-playbook-docs.yml'
workflow_dispatch:
Expand All @@ -28,14 +32,7 @@ jobs:
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
9 changes: 6 additions & 3 deletions build/agent_playbook_docs/check_sdk_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@


def wire_contract(document):
"""Ignore editorial operation text while comparing transport and model schemas."""
result = deepcopy(document)
result["info"].pop("title", None)
result.pop("tags", None)
Expand All @@ -29,6 +30,7 @@ def wire_contract(document):
continue
operation.pop("tags", None)
operation.pop("summary", None)
operation.pop("description", None)
return result


Expand All @@ -39,6 +41,10 @@ def digest(document):

def check(sdk_repo=None):
lock = json.loads(LOCK.read_text())
requirements = Path(__file__).with_name("requirements.txt").read_text().splitlines()
for name in ("python_package", "unified_python_package"):
if lock[name] not in requirements:
raise ValueError(f"Documentation requirements differ from {name} in the source lock.")
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.")
Expand All @@ -52,9 +58,6 @@ def check(sdk_repo=None):
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


Expand Down
28 changes: 19 additions & 9 deletions build/agent_playbook_docs/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import shutil
import sys
from textwrap import dedent
from tempfile import TemporaryDirectory

ROOT = Path(__file__).resolve().parents[2]
Expand All @@ -23,14 +24,14 @@
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"):
if row.split("\t", 1)[0] in ("iris-python", "iris-curl"):
fields = row.split("\t")
CLIENTS[fields[6].rsplit(".", 1)[1]] = fields[1]
assert set(CLIENTS) == {"py", "mts", "sh"}
assert set(CLIENTS) == {"py", "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)
FENCE = re.compile(r"```(?:python|bash)\n(.*?)\n```", re.S)


def source_path(page, suffix, action=None):
Expand Down Expand Up @@ -76,7 +77,7 @@ def calls(page):


def blocks(page):
"""Collect published shared steps and their retained TypeScript test fixtures."""
"""Collect the Python and cURL steps published in each walkthrough."""
result = []
seen = set()
for attrs in calls(page):
Expand All @@ -99,11 +100,12 @@ def scripts(page, 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)]
result = {suffix: "\n\n".join(block[1][i] for block in selected) + "\n"
for i, suffix in enumerate(CLIENTS)}
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))
result["sh"] = skill_package() + "\n\n" + result["sh"]
result["sh"] = "set -euo pipefail\n\n" + result["sh"]
return result


def full_examples():
Expand All @@ -129,14 +131,22 @@ def cli_script(page):

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


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


def connection_setup():
"""Return the quickstart's shell commands for loading a trusted dotenv file."""
for source in re.findall(r"(?ms)^ ```bash\n(.*?)^ ```", (DOCS / "quickstart.md").read_text()):
if "source .env" in source:
return dedent(source)
raise ValueError("The quickstart dotenv loading command is missing")


if __name__ == "__main__":
import argparse

Expand Down
30 changes: 19 additions & 11 deletions build/agent_playbook_docs/live_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json
import os
import re
import shlex
from pathlib import Path
import subprocess
import sys
Expand All @@ -22,7 +23,7 @@

from redis_agent_playbook import Playbook

from examples import AGENT_GUIDES, GUIDES, agent_scripts, cli_script, full_examples, scripts, skill_package, standalone_python
from examples import AGENT_GUIDES, GUIDES, agent_scripts, cli_script, connection_setup, full_examples, scripts, skill_package, standalone_python

HERE = Path(__file__).resolve().parent

Expand Down Expand Up @@ -58,7 +59,7 @@ def cleanup(args, playbook_id, key_ids):
failures = []
targets = [(args.ids_url + "/v1/api-keys/" + key_id, args.control_token)
for key_id in key_ids]
targets.append((args.cp_url + "/v1/playbooks/" + playbook_id, args.admin_token))
targets.append((args.cp_url + "/iris/v1/agent-playbooks/playbooks/" + playbook_id, args.admin_token))
for url, token in targets:
try:
request("DELETE", url, token)
Expand Down Expand Up @@ -128,7 +129,7 @@ def validate_agent_reply(output, skill_id, template_uri):

def run_case(name, case, args, log_dir):
guide, suffix, source = case
created = request("POST", args.cp_url + "/v1/playbooks", args.admin_token, {
created = request("POST", args.cp_url + "/iris/v1/agent-playbooks/playbooks", args.admin_token, {
"name": "docs-validation-" + uuid4().hex[:12],
"embedding": {"provider": args.embedding_provider, "model": args.embedding_model,
"dimensions": args.embedding_dimensions},
Expand All @@ -140,15 +141,14 @@ def run_case(name, case, args, log_dir):
author_key = create_key(args.ids_url, args.control_token, playbook_id, ["read", "write", "publish", "propose"], args.tenant_id, key_ids)
with tempfile.TemporaryDirectory(prefix="playbook-live-") as directory:
directory = Path(directory)
(directory / "node_modules").symlink_to(HERE / "node_modules", target_is_directory=True)
preparation = subprocess.run(["bash", "-euc", skill_package()], cwd=directory,
text=True, capture_output=True, timeout=30)
if preparation.returncode:
raise RuntimeError("Skill package preparation failed")
env = {**os.environ, "PLAYBOOK_URL": args.dp_url, "PLAYBOOK_ID": playbook_id,
"PLAYBOOK_API_KEY": author_key, "OPENAI_AGENTS_DISABLE_TRACING": "1"}
env = {**os.environ, "IRIS_AGENT_PLAYBOOKS_URL": args.dp_url, "IRIS_AGENT_PLAYBOOKS_PLAYBOOK_ID": playbook_id,
"IRIS_AGENT_PLAYBOOKS_API_KEY": author_key, "OPENAI_AGENTS_DISABLE_TRACING": "1"}
env["PATH"] = str(Path(sys.executable).parent) + ":" + env["PATH"]
with Playbook(args.dp_url, playbook_id=playbook_id, api_key=author_key, retry_config=None) as client:
with Playbook(server_url=args.dp_url, playbook_id=playbook_id, api_key=author_key, retry_config=None) as client:
if guide in ("retrieval", "limits", "async_search", *AGENT_GUIDES):
env["PLAYBOOK_FAQ_ID"] = seed(client, directory, "faq")
if guide in ("retrieval", "mcp", *AGENT_GUIDES):
Expand All @@ -159,16 +159,24 @@ def run_case(name, case, args, log_dir):
if resource.path == "references/reply-template.md")
if guide in ("retrieval", "limits", "async_search", "mcp", *AGENT_GUIDES):
reader_key = create_key(args.ids_url, args.control_token, playbook_id, ["read"], args.tenant_id, key_ids)
env["PLAYBOOK_API_KEY"] = reader_key
env["IRIS_AGENT_PLAYBOOKS_API_KEY"] = reader_key
connection = {key: env.pop(key) for key in (
"IRIS_AGENT_PLAYBOOKS_URL", "IRIS_AGENT_PLAYBOOKS_PLAYBOOK_ID",
"IRIS_AGENT_PLAYBOOKS_API_KEY",
)}
env.pop("IRIS_AGENT_PLAYBOOKS_CONTENT_API_KEY", None)
dotenv = directory / ".env"
dotenv.write_text("".join(f"{key}={shlex.quote(value)}\n" for key, value in connection.items()))
dotenv.chmod(0o600)
# A file named mcp.py shadows the installed MCP package.
path = directory / ("mcp_client.py" if name == "mcp.py" else name)
path.write_text(source)
command = {
"py": [sys.executable, str(path)],
"mts": [str(HERE / "node_modules/.bin/tsx"), str(path)],
"sh": ["bash", str(path)],
"cli.sh": ["bash", str(path)],
}[suffix]
command = ["bash", "-euc", connection_setup() + "\nexec " + shlex.join(command)]
completed = subprocess.run(command, env=env, cwd=directory, text=True,
capture_output=True, timeout=180)
output = completed.stdout + completed.stderr
Expand All @@ -179,11 +187,11 @@ def run_case(name, case, args, log_dir):
if completed.returncode:
raise RuntimeError(f"Exit {completed.returncode}; see {name}.log")
if name.startswith("quickstart_copy"):
entries = client.list_faqs().faqs
entries = client.list_faqs().items
if not any(entry.latest_version == 2 and entry.publication.version == 2 for entry in entries if entry.publication):
raise RuntimeError("Quickstart copy did not retain its updated publication")
if name.startswith("skills_copy"):
if not any(entry.publication for entry in client.list_skills().skills):
if not any(entry.publication for entry in client.list_skills().items):
raise RuntimeError("Skill copy did not retain a publication")
if guide == "mcp":
context = json.loads(completed.stdout[completed.stdout.index("{"):])
Expand Down
Loading
Loading