Contract-first Python SDK + CLI for VMware Private AI Service (PAIS), with a bundled mock server for offline development. Build and test against PAIS APIs without a live host; switch to a real instance via config.
Not on PyPI yet — install straight from GitHub. The pais command is the console script.
# pip — latest main
pip install "git+https://github.com/dshahnaz/pais-sdk-cli.git"
# pip — pinned to a tag/commit (recommended for reproducibility)
pip install "git+https://github.com/dshahnaz/pais-sdk-cli.git@v0.7.0"
# pip — include dev extras (adds the HuggingFace tokenizers dep required by the test-suite splitters)
pip install "git+https://github.com/dshahnaz/pais-sdk-cli.git#egg=pais-sdk-cli[dev]"
# pip — with optional contextual-retrieval extra (Anthropic SDK for --with-context-llm)
pip install "git+https://github.com/dshahnaz/pais-sdk-cli.git#egg=pais-sdk-cli[dev,contextual]"
# uv — into an isolated tool environment (recommended for CLI users)
uv tool install "git+https://github.com/dshahnaz/pais-sdk-cli.git"
uv tool install --with "pais-sdk-cli[dev]" "git+https://github.com/dshahnaz/pais-sdk-cli.git"
# pipx — same idea
pipx install "git+https://github.com/dshahnaz/pais-sdk-cli.git"Verify:
pais --helpFor local development (clone + editable install) see CONTRIBUTING.md.
# run the mock server
python -m pais_mock &
# use the CLI against the mock
export PAIS_MODE=http
export PAIS_BASE_URL=http://localhost:8080/api/v1
export PAIS_AUTH=none
pais kb listRun pais (no args) in a terminal and it opens with a smart landing screen:
$ pais
profile=lab · mode=http (real PAIS)
3 KBs · 5 indexes · 2 agents · 0 drift
recommended: 💬 Chat with an agent
> 💬 Chat with an agent
🤖 Set up a chat agent over my docs
📦 Provision KB + index (no agent)
🔧 Apply pending TOML config
📥 Ingest data into an index
🔎 Search an index (no LLM)
🗑 Cleanup (delete KB / index / agent)
📋 all commands…
The recommended workflow is chosen from your env state (no agents → "Set up an agent"; drift detected → "Apply pending TOML"). Each workflow walks you through the steps with pick-or-create lists (showing existing items + + create new), single-screen reviews of all defaults (with one-line hints — e.g. chunk_size 512 ↑ tokens, not chars), and a "what next?" menu after success.
Set up an agent is the headline flow:
🤖 Set up a chat agent over my docs.
1. Pick or create a KB
2. (optional) save it as alias `prod_docs` in pais.toml
3. Pick or create an index under it
4. (optional) save the index alias too
5. Create the agent (index_id + index_top_n, doc-aligned)
6. → Ingest data into this index now (recommended — index is empty)
💬 Chat with it
📊 View `pais status`
✅ Done
Destructive ops use type-to-confirm (you type the resource name to proceed). --quick-confirm / -Q falls back to y/N for power users.
pais shell opens the menu explicitly. To opt out of the bare-pais trigger, pass --no-interactive or set PAIS_NONINTERACTIVE=1. Non-TTY callers (pipes, scripts, CI) always print the help banner, never the menu. Pick 📋 all commands… from the landing screen for the full v0.5-style flat command list.
Navigation shortcuts:
Ctrl-C/Esc→ back to the previous step (or exit shell at the landing screen). Every prompt shows the hint inline.← backrow in every picker — same effect asCtrl-C, just clickable.- Verbosity tiers (applies to both the shell and any
pais <cmd>subcommand):- no flag →
WARNING— only warnings/errors (TLS-verify-off, purge fallback, retries). Silent happy path. -v→INFO— high-signal events (ingest start/done, index recreated, etc.).-vv→DEBUG— per-request HTTP traces with latency + status. The full log always rotates to~/.pais/logs/pais.logregardless of the tier you pick for stderr.
- no flag →
Tired of export PAIS_* on every shell? Drop a TOML config file with named profiles:
pais config init # writes ~/.pais/config.toml with comments
pais config init --project # writes ./pais.toml in the current dir
pais config show --profile lab # print effective settings (secrets redacted)
pais config path # which file + profile resolve right nowExample file:
# ~/.pais/config.toml (or ./pais.toml — project wins over global)
default_profile = "lab"
[profiles.lab]
mode = "http"
base_url = "https://pais.internal/api/v1"
auth = "none"
verify_ssl = false
[profiles.prod]
mode = "http"
base_url = "https://pais.example.com/api/v1"
auth = "oidc_password"
oidc_issuer = "https://pais.example.com"
client_id = "pais-cli"
username = "alice"
# password / client_secret / bearer_token are REJECTED here — env vars only.Then every command picks it up:
pais --profile lab kb list
pais --profile prod agent chat agent_xx "hello"
pais --profile prod agent chat agent_xx --file prompt.md # long prompts from a file
# or set PAIS_PROFILE=lab once for the whole shellInside the interactive chat (pais chat), type /file <path> at the you: prompt to send a file's contents as your message instead of typing it in.
Precedence (highest first): CLI flag → PAIS_* env var → config file → defaults. Discovery order for the file: --config <path> → PAIS_CONFIG → ./pais.toml → ~/.pais/config.toml.
export PAIS_MODE=mock
uv run pais kb create --name demo
uv run pais kb listTests use the in-process fake transport; no server needed.
export PAIS_MODE=http
export PAIS_BASE_URL=https://pais.internal/api/v1
export PAIS_AUTH=none
export PAIS_VERIFY_SSL=false
uv run pais kb listexport PAIS_MODE=http
export PAIS_BASE_URL=https://pais.example.com/api/v1
export PAIS_AUTH=oidc_password
export PAIS_OIDC_ISSUER=https://pais.example.com
export PAIS_CLIENT_ID=... PAIS_USERNAME=... PAIS_PASSWORD=...
uv run pais kb listDeclare KBs + indexes + their splitters in TOML, then operate on short aliases instead of UUIDs.
# ~/.pais/config.toml (or ./pais.toml — project wins)
[profiles.lab]
mode = "http"
base_url = "https://10.160.11.45/api/v1"
auth = "none"
verify_ssl = false
[profiles.lab.knowledge_bases.test_suites]
name = "mops-permanent-test-suites"
data_origin_type = "LOCAL_FILES"
[[profiles.lab.knowledge_bases.test_suites.indexes]]
alias = "main"
name = "ts-idx"
embeddings_model_endpoint = "BAAI/bge-small-en-v1.5"
chunk_size = 512
chunk_overlap = 64
[profiles.lab.knowledge_bases.test_suites.indexes.splitter]
kind = "test_suite_bge" # per-test-case chunks + breadcrumb; 400-tok budget
max_case_tokens = 400
[[profiles.lab.knowledge_bases.test_suites.indexes]]
alias = "arctic"
name = "ts-arctic"
embeddings_model_endpoint = "Snowflake/snowflake-arctic-embed-m-v2.0"
chunk_size = 2048
chunk_overlap = 256
[profiles.lab.knowledge_bases.test_suites.indexes.splitter]
kind = "test_suite_arctic" # wider 1500-tok budget for Arctic's 8 K windowThen:
pais --profile lab kb ensure # create anything missing on the server (idempotent)
pais kb list --with-counts # see KBs with index + document totals
pais kb show test_suites # full KB detail with per-index breakdownpais kb ensure is idempotent. Re-run after editing the TOML — it adds new KBs/indexes and warns about server-side mismatches PAIS doesn't expose updates for. --dry-run previews; --prune --yes deletes server-side resources not in the TOML (per-item confirmation).
Generic pais ingest runs the splitter declared on the target index over any file or directory.
# 0. ensure the KB + index exist (one-time setup from config)
pais --profile lab kb ensure
# 1. ingest a directory of suite markdown files (uses test_suite_bge splitter from config)
pais ingest test_suites:main ./suites/
# 2. same files into an index backed by a different embedding model
pais ingest test_suites:arctic ./suites/
# 3. re-ingest only changed suites; other suites in the index stay
pais ingest test_suites:main ./changed/ --replace
# 4. preview without uploading
pais ingest test_suites:main ./suites/ --dry-run
# 5. one-off override of the splitter (pick the variant matching the index's embeddings model)
pais ingest test_suites:main ./suites/ --splitter test_suite_arctic
# 6. wait for indexing
pais index wait test_suites:mainUUIDs work everywhere aliases do — pais ingest <kb_uuid>:<idx_uuid> ./files/ is fine for ad-hoc use.
| kind | summary | target embeddings model | chunk_size | chunk_overlap |
|---|---|---|---|---|
test_suite_bge |
Per-test-case chunks with breadcrumb; tuned for bge-small-en-v1.5 | BAAI/bge-small-en-v1.5 |
512 | 64 |
test_suite_arctic |
Per-test-case chunks with breadcrumb; tuned for arctic-embed-m-v2.0 | Snowflake/snowflake-arctic-embed-m-v2.0 |
2048 | 256 |
Both emit 1 overview chunk + 1 chunk per test case, each with a compact 2-line breadcrumb prepended so retrieval can't lose the suite/case anchor. Pick the variant matching the index's embeddings_model_endpoint. See docs/ingestion.md for the full story (two-layer chunking, why the breadcrumb lives inside the body, optional Anthropic contextual retrieval).
Inspect + preview from the CLI:
pais splitters list # compact: kind + summary
pais splitters show test_suite_bge # full meta + suggested index config
pais splitters preview test_suite_bge ~/suites/foo.md # stats + sample chunk
pais splitters preview test_suite_bge ~/suites/foo.md --dump /tmp/preview # writes every chunk to disk
pais splitters preview test_suite_bge ~/suites/foo.md --show-all # prints each chunk inline
pais splitters new my_custom # scaffold a new splitterThe preview --dump <dir>/ flag is the fastest way to answer "will each of my 300 test suites chunk correctly?" — dump the first handful, open the files, verify the breadcrumbs + sizes, then bulk-ingest with confidence.
Content hygiene: bodies are uploaded as-is. Scrub internal hostnames / IPs / credentials from input files before ingesting into a shared PAIS deployment — the structured logger redacts secret-looking keys but cannot sanitize arbitrary prose.
Destructive ops require either a TTY confirmation prompt or --yes / -y. They refuse to run in scripts (non-TTY) without --yes.
All commands accept either an alias (from your config) or a UUID.
# delete a whole KB (cascades indexes + documents)
pais kb delete test_suites --yes
# keep the KB, drop every document under every index in it
pais kb purge test_suites --yes
# keep the index, drop its documents
pais index purge test_suites main --yes
# delete one index entirely
pais index delete test_suites main --yes
# cancel a running indexing job
pais index cancel test_suites main --yesEach cleanup/cancel command takes --strategy {auto,api,recreate}:
api— try the obvious REST verb (DELETE /documents/{id}for purge,DELETE /active-indexingfor cancel). Fails fast if the PAIS deployment doesn't expose it.recreate— delete the index entirely and recreate it with the same config. Always works but the new index gets a differentid— you'll need to re-link any agents pointing at the old one. The CLI prints a warning when this happens.auto(default) — tryapifirst, fall back torecreateon 404/405.
pais ingest --replace deletes only the documents whose origin_name matches the splitter's group_key for each input file; everything else stays:
pais ingest test_suites:main ./changed-suites/ --replace- Logs:
~/.pais/logs/pais.log(rotating, 5MB × 3). - Verbosity:
PAIS_LOG_LEVEL=DEBUG. - Secrets (
authorization,password,access_token,refresh_token, ...) are redacted. Safe to share log files as-is. - Every request carries a
request_idthat round-trips to the server asX-Request-IDfor correlation.
CLI (typer) → SDK (resources) → Transport (httpx | fake) → PAIS host | mock server
Models in src/pais/models/ are imported by both the SDK and the mock server — the mock serves the exact contract the SDK validates.
MIT