Skip to content

About

Runnable scenario and presentation demos for the Donkey Development Kit (DDK) — companion to Donkey-Development-Kit/donkey-development-kit.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Donkey Development Kit (DDK) — Demos

Runnable demos for the Donkey Development Kit (DDK).

This repo holds two suites on purpose. They cover the same SDK, but they are not interchangeable:

demos/claude-made/ demos/human-made/
For A room, a recording, or CI that must stay offline A terminal you type in, or paste from
Shape Numbered narrative demos (01–15), each with demo.py + README Straight-line scripts, one folder per framework, one file each
Runner make demo N=03, make offline, _harness (redact, mock, pause) python "demos/human-made/<framework>/….py" — not in make
Network Fourteen of the fifteen need nothing. Demo 09 is live-only Most need DONKEY_LLM_PROXY_*. start-gateway, gateway-unavailable and simulated scripts need no gateway

There is also a browser demo, the feature showcase: a LangGraph chat on one side and a clickable catalog of DDK features on the other, running against donkey mock. Try the static replay on GitHub Pages, or run it live locally with demos/showcase/run.sh.

For the per-framework reference snippets (one main.py per framework, CI-gated), see python/examples/ in the SDK repo. Those are not these demos.

What is still missing from either suite is tracked in roadmap.md.

Presenting the narrative set? Read PRESENTING.md — runbook, talk tracks, failure playbook, and screen-recording rules.

python -m pip install -e ".[sdk]"   # inside a venv — see Setup
make offline                # every claude-made demo that needs nothing

Claude-made — the narrative suite

Each demo is a story with acts, a projector-safe narrator, and output masking. run.py discovers **/demo.py under demos/, so only this suite is listed by make list / make offline.

Fourteen of the fifteen run with no credentials and no gateway, against the SDK's local simulator — the same captured responses the SDK's own tests are written against.

# Demo Shows Needs
01 governed client Stock vs governed client (async and sync=True), then @donkey.governed / @donkey.tool nothing
02 typed refusals Captured rejection shapes through classify(), plus GatewayUnavailable nothing
03 budget and pacing The token window as an object; pace(reserve=) and wait_for_reset() (only when reset_at is known) nothing
04 simulating refusals donkey.simulate(), donkey mock --scenario, and start_gateway() nothing
05 conformance suite pytest --donkey-conformance (and donkey test) grading a naive agent, then the fixed one nothing
06 telemetry GenAI spans (gen_ai.* + donkey.*), donkey.run(id=…), zero-config OTLP (no implicit global provider) nothing
07 model handles Honest gaps: no /models catalog; points at donkey doctor (demo 14) nothing
08 framework objects One deep adapter, seven at connection_kwargs() (Agents SDK: {openai_client}), an Agent Framework Agent whose policy_middleware() ends a simulated PII refusal as PIIDetected nothing
09 LangGraph agent A real tool-calling loop, governed end to end credentials
10 last_call Gateway identity, routing/usage on a 200 (model-based and semantic); ModelSubstituted when you opt in nothing
11 donkey init Commented .donkey-kit.toml, every gap named at once, no secrets on disk nothing ([cli])
12 ToolSet.filter Independent filtered views of MCP tools; binding still blocked on verification nothing
13 JWT / model-wallet llm_proxy_auth="jwt": X-Client-Id + rotating JWT, no client_secret nothing ([llm])
14 donkey doctor Wrong URL / wrong creds / model-not-allowed, against the local simulator nothing ([cli] [local])
15 provider passthrough Per-provider request id, Azure vs Bedrock guardrails, OpenAI vs Gemini error envelopes, ingress Formats nothing

Every claude-made demo takes --target mock (default) or --target live. Demo 09 is live only: the simulator replays a captured completion and will not decide to call tools. The LangGraph adapter calls /chat/completions (use_responses_api=False), the one route every upstream behind an OpenAI-format proxy serves.

make list                   # claude-made table, from the filesystem
make demo N=03              # one narrative demo
make offline                # all fourteen offline claude-made demos
make doctor                 # what is installed, what will therefore run
make mock                   # simulator in the foreground, for a second pane

DEMO_PAUSE=1 make demo N=03 # pause between acts — use this when presenting

python run.py 03 works too, and each demo is a plain script (python demos/claude-made/03_budget_and_pacing/demo.py) once the repo is installed.

Human-made — the straight-line scripts

Short, top-to-bottom Python under demos/human-made/, one folder per framework. No _harness, no redaction, no make target. Run with the same environment you already use for the SDK. They do not read .env.local, so export it first (see Filling the file). Filenames contain spaces; quote the path. Each folder has a README with its install line, required environment and what every script prints — start at demos/human-made/README.md.

python "demos/human-made/openai/02 - basic-responses-gw.py"
python "demos/human-made/openai/15 - start-gateway.py"       # local simulator
python "demos/human-made/langgraph/09 - start-gateway.py"    # local simulator

How each framework reaches the gateway differs, and the scripts print it rather than hide it:

Folder Route X-Correlation-Id per run donkey.last_call Typed refusal
openai /responses yes observed classify(err.response)
langgraph /chat/completions yes unobserved (LangChain's task) donkey.langgraph.typed_refusals()
openai-agents /responses yes unobserved (Runner's task) classify(err.response)
agent-framework /chat/completions yes observed classify(err.__cause__.response)
strands /chat/completions yes observed classify(err.response)
crewai /chat/completions no unavailable classify(err.response)
llamaindex /chat/completions yes observed classify(err.response)
adk /chat/completions (LiteLLM) yes in after_model_callback (Runner's task) classify() in on_model_error_callback
anthropic /v1/messages (Format=Anthropic) yes observed classify(err.response)
gemini :generateContent (Format=Gemini, plain httpx) — — classify(response)

The typed-refusal column is the explicit pattern each script uses; it works on SDK 0.1.1 and later. From SDK 0.1.2, an error leaving donkey.run() or @donkey.governed is already typed (PIIDetected and the rest, with the framework's own error on exc.framework_error), and donkey_kit.typed_refusals() does the same around any block. ADK's model() is the exception: LiteLLM rebuilds the response, so 02 keeps its callback.

From SDK 0.1.2 the local simulator serves /chat/completions as well as /responses. The /chat/completions folders predate that and have no simulator script; strands/03 uses in-process simulate() instead.

openai/

# Script Shows Needs
01 basic-responses-no-gw Stock OpenAI, no gateway OPENAI_API_KEY
02 basic-responses-gw donkey.openai() + last_call on a 200 proxy creds
03 typed-refusals-simulated simulate() loop, including ContentSafetyBlocked proxy creds (no network)
04 typed-refusals-live Live refusals, blocking client, no async proxy creds + policies
05 budget_and_pacing pace(reserve=) then wait_for_reset() proxy creds
06 otel exporter simple Host-owned TracerProvider (Donkey rides it) proxy + OTLP
07 otel exporter advanced Several donkey.run(team=…, project=…) + a refusal span proxy + OTLP
08 last-call Full last_call record; on_model_substitution="raise" proxy creds
09 governed-and-tool @donkey.governed and @donkey.tool nothing
10 zero-config-otlp Donkey.from_env() exports Donkey's own spans over OTLP when the env var is set proxy creds
11 gateway-unavailable Dead origin → GatewayUnavailable as __cause__ nothing
12 streaming responses SSE; last_call usage after the terminal event proxy creds (live)
13 regex-and-content-safety Live regex-prompt-guard and Azure content-safety proxy + those policies
14 jwt-wallet llm_proxy_auth="jwt" + StaticToken (async-only) wallet URL, DONKEY_LLM_PROXY_WALLET_CLIENT_ID, DONKEY_LLM_JWT
15 start-gateway start_gateway() + stock httpx; same fixtures as 03 nothing ([local])
16 bedrock-guardrails Live Bedrock Guardrails → ContentSafetyBlocked; request_id from x-amzn-requestid proxy + Bedrock Guardrails

langgraph/

# Script Shows Needs
01 basic-no-gw Stock ChatOpenAI, no gateway OPENAI_API_KEY
02 basic-gw donkey.langgraph("gpt-4o"); usage from usage_metadata proxy creds
03 typed-refusals-simulated simulate() through a create_agent graph + typed_refusals() proxy creds (no network)
04 typed-refusals-live PII / unknown model / bad creds out of a graph proxy creds + policies
05 agent-and-tool Tool sees the donkey.run() id with nothing threaded through state proxy creds (live)
06 otel exporter simple Host TracerProvider, two governed calls proxy + OTLP
07 streaming astream; usage on the terminal chunk proxy creds (live)
08 gateway-unavailable GatewayUnavailable two causes down nothing
09 start-gateway Graph against start_gateway(), every 2nd call PII nothing ([local])

openai-agents/

# Script Shows Needs
01 agent-and-tool OpenAIResponsesModel + a tool; usage from context_wrapper proxy creds
02 typed-refusals-simulated simulate() through Runner.run proxy creds (no network)
03 start-gateway Agent against start_gateway(), every 2nd call PII nothing ([local])

agent-framework/

# Script Shows Needs
01 basic-gw Agent(client=donkey.agent_framework.chat_client(...)) proxy creds
02 typed-refusals-live ChatClientException → classify(__cause__.response) proxy creds + policies
03 start-gateway Agent against start_gateway(), every 2nd call PII nothing ([local])

strands/

# Script Shows Needs
01 basic-gw OpenAIModel(client=donkey.openai()) — see the note below strands-agents[openai] + proxy
02 agent-and-tool Tool loop; run id in the tool; last_call observed proxy creds (live)
03 typed-refusals-simulated simulate() through invoke_async (no 429: Strands retries it) proxy creds (no network)
04 typed-refusals-live PII / unknown model / bad creds proxy creds + policies

crewai/, llamaindex/, adk/

# Script Shows Needs
01 basic-gw crewai.llm(...).call and Agent.kickoff proxy creds
02 typed-refusals-live PII / unknown model / bad creds proxy creds + policies
01 basic-gw OpenAILike complete and chat proxy creds
02 typed-refusals-live PII / unknown model / bad creds proxy creds + policies
01 basic-gw LiteLlm agent through InMemoryRunner proxy creds
02 refusal-live PII 403 typed as PIIDetected in on_model_error_callback proxy creds + PII policy

anthropic/, gemini/

# Script Shows Needs
01 native-messages donkey.anthropic.client() on /v1/messages; request-id vs request_id Format=Anthropic proxy
02 typed-refusals-simulated simulate() through the native Anthropic client proxy creds (no network)
01 native-generate-content :generateContent via httpx (no adapter ships) Format=Gemini proxy
02 openai-shape-rejected Gemini's list envelope → UpstreamRequestError Format=Gemini proxy

Strands note. donkey.strands.model() hands Strands client_args, and Strands opens and closes an OpenAI client from them on every request — which closes the SDK's shared transport after the first call. The scripts pass a pre-built client=donkey.openai() instead, which Strands reuses and leaves open.

Setup

# 0. A virtual environment (see below for why this is not optional)
python3 -m venv .venv          # .venv/ is git-ignored
source .venv/bin/activate      # once per terminal

# 1. The demo harness (adds _harness to the path; no SDK pinned)
python -m pip install -e .

# 2. The SDK. Either the release this branch tracks, from PyPI…
python -m pip install -e ".[full]"      # donkey-kit 0.1.3 + langchain

#    …or your own checkout, on the matching SDK branch (main here)
python -m pip install -e "../donkey-development-kit/python[llm,local,test,otel,langgraph,cli]"
python -m pip install "langchain>=1.0"   # demo 09 only; no donkey-kit extra ships it

Which SDK each branch tracks. main (this branch) runs against the SDK's latest release (main, on PyPI), so here [sdk] and [full] pin it exactly. develop tracks the SDK's develop, so there they pin the git ref @develop. Don't mix the two: module paths and CLI flags change between releases. CI enforces this: the SDK alignment workflow checks the pin (make check-sdk) and runs make offline against it on every PR, and nightly on both branches, so an SDK release or a breaking SDK develop change shows up the next morning.

Why a virtual environment. Homebrew's python3 (and most Linux distro Pythons) is marked externally managed (PEP 668), so a global pip3 install … stops with error: externally-managed-environment. Do not work around it with --break-system-packages; install into .venv instead. Without it, the demos fail with ModuleNotFoundError: No module named 'openai' or 'donkey_kit', and make doctor reports every package as FAIL. Homebrew ships no bare pip or python command, only pip3 and python3; both short names work once .venv is active.

Quote the [...] extras: zsh, the macOS default shell, treats unquoted brackets as a glob and fails with no matches found. uv users can replace the first two lines with uv venv and pip install with uv pip install.

To have Cursor / VS Code activate the environment in every new terminal, set "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python" in your local .vscode/settings.json, which you copy from settings.shared.json.

Live demos additionally need three variables. Copy the template and fill it in:

cp .env.example .env.local     # .env.local is git-ignored

A shell export always beats a file value, so you can skip the file entirely. make doctor will tell you what it found without printing any of it.

Filling the file from the provisioned proxies

The proxies these demos talk to are the ones created by donkey-development-kit-provisioning and exercised by donkey-development-kit-acceptance. Take the values from those sibling checkouts instead of minting new ones:

Variable Where it comes from
DONKEY_LLM_PROXY_URL base_url of a [[proxy]] entry in ../donkey-development-kit-acceptance/acceptance/proxies.toml, plus a trailing /
DONKEY_LLM_PROXY_CLIENT_ID / _SECRET The client_id_env / client_secret_env names on that same entry, looked up in ../donkey-development-kit-acceptance/.env
OPENAI_API_KEY (human-made 01 only) OPENAI_KEY in ../donkey-development-kit-provisioning/.env
DEMO_MODEL The model on that same entry, e.g. openai/gpt-5-mini

Start with openai-model-routing: it is the one tagged responses, streaming and attribution, which is what most demos exercise. Only one proxy can be active at a time, so keep the others as commented-out blocks and swap by uncommenting. injection-guard and azure-content-safety are the ones human-made 04 refuses against; token-rate-limit is the only proxy that emits the x-llm-proxy-ratelimit header human-made 05 reads.

The provisioning .env is Key: value, the acceptance .env is KEY=value, and neither can be copied wholesale into this one. Do not print either file while screen-sharing: the provisioning one holds Anypoint, provider and Keycloak admin secrets.

The human-made scripts do not load .env / .env.local. Only _harness does, and the SDK never reads dotenv files on its own. Export the file into your shell before running them:

set -a; source .env.local; set +a
python "demos/human-made/openai/02 - basic-responses-gw.py"

The provisioned proxies route gpt-5-mini only, while the human-made scripts hardcode model="gpt-4o". Expect a routing refusal or a ModelSubstituted until the script's model matches the proxy's.

Credentials never leave your machine

The claude-made suite is built on the assumption that a demo will be screen-shared, recorded, and committed to by someone in a hurry. Four things guard that:

  • Output is masked by default. Every value a narrative demo prints goes through _harness/redact.py, which masks gateway hostnames, the client_id/client_secret pair, and the tenant identifiers that ride along in captured responses (x-envoy-decorator-operation, openai-organization, cf-ray). Header names are always shown; their values never are.
  • DEMO_REDACT=0 announces itself. Turning masking off prints a warning banner in the run context of every narrative demo.
  • No captured traffic is vendored. Demo 02 loads fixtures from the installed SDK (donkey_kit.simulator.fixtures) rather than keeping copies here.
  • make scan reads content, not filenames. scripts/scan_secrets.py fails on assigned credential values, Anypoint instance ids, bearer tokens, UUIDs and non-allowlisted hostnames. make hooks installs it as a pre-commit hook.

Human-made scripts do not redact. They print completions and error strings as the SDK returned them. Use them on a private terminal, not on a shared recording.

make scan       # everything tracked
make hooks      # then it runs on every commit

Honest status

The DDK is evolving fast, and these demos change with it. Scripts, extras and expected output can shift between SDK releases, sometimes within days. When something here disagrees with the SDK, the SDK wins: check the DDK main repository — its commit history, docs/verified-apis.md and python/examples/ — before assuming a demo is right.

The proxy contract these demos exercise is live-verified: the base URL shape (no /v1), the client_id/client_secret header pair, streaming, inbound X-Correlation-Id echo, the Anthropic- and Gemini-native ingress routes, and seven rejection shapes (auth, PII, token rate limit, upstream including Gemini's list envelope, regex prompt guard, Azure content-safety, Bedrock Guardrails). See docs/verified-apis.md.

Three things the demos say out loud rather than gloss over:

  • Happy-path budget numbers on the simulator are illustrative. A live 200 (with the token-rate policy applied) carries the window as prose x-llm-proxy-ratelimit — that sentence is live-verified. The numeric x-token-* trio is verified on the 429. Claude-made 03 prints the distinction.
  • Header-based injection-protection is the one guardrail still typed from the documented wire shape — no proxy running that policy is deployed to capture. Regex prompt guard and Azure content-safety are live (2026-09-22), Bedrock Guardrails too (2026-09-24). An undiscriminated content-moderation 4xx still falls through to a generic PolicyViolation. simulate(PromptInjectionBlocked) injects the documented injection-protection representative, not the live regex capture — one fixture per exception type.
  • Only one framework adapter has reached live-verified status; the rest have not. The raw client and LangGraph are conformance-tested against the simulator; every other adapter constructor, ADK's model() included, is signature-confirmed offline by the SDK's scripts/verify_frameworks.py; ADK's gemini() is live-verified through a Format=Gemini proxy. An adapter raises "blocked on verification" only when the installed framework version lacks the class or field it depends on. Cost tags live on donkey.cost.* spans; the gateway has no inbound cost-tag ingestion (verified-negative).

Not demoed at all, because the SDK raises NotImplementedError("blocked on verification"): Exchange discovery, MCP tool binding, and every provisioning verb except validate and mock. Do not add demos for those until the SDK unblocks them — see roadmap.md.

About

Runnable scenario and presentation demos for the Donkey Development Kit (DDK) — companion to Donkey-Development-Kit/donkey-development-kit.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages