Skip to main content

PromptFlip — the SDK that fetches prompts without your code throwing, plus the `pf` local-first authoring CLI.

Project description

PromptFlip — the pf CLI + Python SDK

Version prompts like code. Ship them without deploys.

Website · Docs · TypeScript SDK

One install, two tools:

  • the pf CLI — author and version prompts as local files, git-style. Fully offline, no account needed;
  • the SDK — your app fetches those prompts at runtime — from the local folder or the cloud — and never crashes if anything is unreachable.

Install

pip install promptflip      # the `pf` command + `from promptflip import PromptFlip`

Requires Python ≥3.10. One dependency: httpx.

1 · Author prompts with pf (offline, no account)

pf versions prompts the way git versions code:

pf config user.name "You"       # once: your author identity
pf config user.email "you@x.io"

pf init my-workspace            # start a repo
pf new support-agent            # creates prompts/support-agent.prompt.md — edit it
pf commit -m "first draft"      # snapshot a version into the local store

pf log support-agent            # history · pf diff — working vs committed
pf checkout support-agent 1     # roll back · pf render — print the resolved text

Everything lives in one portable .promptflip/ directory of plain files — your history is yours, no lock-in, nothing leaves your machine.

2 · Serve them from your app (SDK — local, no API key)

The SDK reads the same files pf manages. No API key, no network:

from promptflip import PromptFlip

pm = PromptFlip(local_dir=".promptflip")

prompt = pm.get(
    "support-agent",
    fallback="You are a helpful support agent for [[plan]]-plan users.",
    inputs={"plan": user.plan},
)

get always returns a usable Prompt — local file, else your fallback — and never throws. Edit → pf commit → your app serves the new version on the next fetch.

3 · Go cloud when you're ready

Sync the same repo to the cloud — team dashboard, environments, publishing, A/B experiments, telemetry:

pf login                        # device-code login in your browser
pf remote --create my-workspace # create the cloud workspace and link this repo
pf push                         # first push imports your whole local history
pf publish support-agent --env prod   # make its tip live in that environment

Then point the SDK at the cloud with an API key from the dashboard:

pm = PromptFlip(api_key="sk_...", environment="prod")

prompt = pm.get(
    "support-agent",
    fallback="You are a helpful support agent for [[plan]]-plan users.",
    inputs={"plan": user.plan},
    assignment_key=user.id,       # sticky A/B variant assignment
)

# Use it with your LLM, report telemetry in one block:
with prompt.run() as r:
    response = openai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "system", "content": prompt.text},
                  {"role": "user", "content": user_msg}],
    )
    r.tokens = response.usage.total_tokens
    r.finish_reason = response.choices[0].finish_reason
    r.converted = True            # any attribute name → custom metric
# auto-sends tokens, finish_reason, latency_ms, converted

# Or report metrics directly (inline non-LLM, or from a worker later):
pm.send(prompt, csat_score=5)

Anyone on the team edits and publishes from the dashboard — changes go live in seconds, with no deploy. Keep local_dir set and the local repo doubles as the offline fallback:

pm = PromptFlip(api_key="sk_...", local_dir=".promptflip")

The three primitives

That's the entire surface a caller writes against:

pm.get(key, fallback=None, *, inputs=None, assignment_key=None, version=None) -> Prompt
pm.send(prompt_or_id, **metrics)
with prompt.run() as r: ...

Hybrid resolution (cloud + local-first)

get resolves in order — cloud (when an api_key is set and reachable) → local repo (when local_dir is set) → fallback string → empty. Every Prompt reports where it came from via prompt.source (cloud / local / fallback / none).

When both local_dir and cache_dir are set, a cached cloud value and the local file are reconciled by recency: if you've edited the local file more recently than the cached cloud content last changed, the local copy wins (a background revalidation that finds no change does not count as "newer"). A live cloud fetch always wins when the service is reachable.

# Optional cross-process cache + version pin:
pm = PromptFlip(api_key="sk_...", cache_dir="~/.cache/promptflip", cache_ttl=300)
prompt = pm.get("support-agent", version=42)   # pin a specific version

Guarantees

  • Never throws — including construction. get always returns a Prompt; send always returns. A missing api_key is not an error — it selects local-only mode.
  • Offline fallback. When the service is unreachable, get serves the last-good cached entry (in-memory or, with cache_dir, on disk), then the local repo, then your fallback template (with [[inputs]] filled client-side so the end-user sees the same string online or offline).
  • Optional disk cache. Set cache_dir to persist cloud fetches across processes with a per-entry TTL, stale-while-revalidate, and ETag revalidation.
  • No private code. The SDK only talks to the public /v1 HTTP contract. An import-ban test enforces zero references to private product code, and the CLI never loads on import promptflip.
  • One runtime dependency: httpx.

Error handling

The SDK never throws on the serving path. Failures emit structured WARNING logs to the promptflip logger; callers attach a logging.Handler to react:

import logging

class CreditAlert(logging.Handler):
    def emit(self, record):
        if getattr(record, "kind", None) == "out_of_credits":
            pagerduty.trigger("PM out of credits")

logging.getLogger("promptflip").addHandler(CreditAlert())

Log records carry structured extra={"kind", "prompt_key", "status_code", "missing_names"}. kind is one of transport, out_of_credits, quota_exceeded, missing_inputs, missing_vars, missing_variables, missing_inputs_in_fallback, invalid_metric, not_found, unresolved, unknown.

Fallback is a template

fallback uses the same [[placeholders]] syntax as the stored prompt; the SDK interpolates client-side from inputs so online and offline render identically. {{vars}} are server-side (potentially secrets) — they are treated as plain text in the fallback. Don't put {{vars}} in your fallback.

Pre-flight check for tests:

from promptflip import check_fallback

missing = check_fallback("Hello [[name]] on [[plan]]", inputs={"name": "Alice"})
# missing == ["plan"]

pf reference

Everything lives in one portable .promptflip/ directory:

.promptflip/
├── prompts/<slug>.prompt.md   # the working files you edit (Markdown + frontmatter)
├── versions/                  # full local history — plain JSON, yours to keep
├── config                     # cloud-workspace binding
├── lock.json                  # (when synced) last-synced remote pointer
└── cache/                     # (when synced) remote mirror — gitignored

Your history is plain files in versions/, so there's no lock-in: only lock.json + cache/ are provider-specific, and dropping them never costs you a prompt or a version.

To work against a workspace that already exists in the cloud, either clone it fresh or link + pull (git-style — you must hold the latest remote state before pushing):

pf clone my-workspace           # fresh local repo, already in sync

# …or, to attach an existing local repo to an existing workspace:
pf remote my-workspace          # link the cloud workspace (does not pull)
pf pull                         # bring down the remote's latest state
pf push                         # refused until you're in sync ("run pf pull first")

{{var}} placeholders resolve from your environment, [[input]] are runtime inputs; pf render <slug> prints the resolved text with no model call. Rolling back is pf checkout <slug> <version>pf commitpf push. Run pf help <command> for any command's full usage.

Tab completion

pf completion install          # wires it into ~/.bashrc or ~/.zshrc (auto-detects)
# then restart your shell, or: source ~/.bashrc

Completes subcommands, and prompt slugs for rm/checkout/log/diff/render. (pf completion bash|zsh just prints the raw script if you'd rather wire it up yourself.) Output is colorized on a terminal; set NO_COLOR to disable.

Extra headers (gateways in front of the API)

If your requests must pass a gateway that expects its own credential headers (a corporate proxy, or an access-gated non-production environment), attach extra headers to every request — either per client:

pm = PromptFlip(api_key="…", extra_headers={"X-Gateway-Token": "…"})

or ambiently for the SDK and the pf CLI via an env var holding a JSON object:

export PF_EXTRA_HEADERS='{"X-Gateway-Token": "…"}'

Unset means no extra headers. Reserved headers the client sets itself (Authorization) always win. A malformed value fails loudly in the CLI and warns (without throwing) in the SDK.

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

promptflip-0.2.1.tar.gz (73.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

promptflip-0.2.1-py3-none-any.whl (84.6 kB view details)

Uploaded Python 3

File details

Details for the file promptflip-0.2.1.tar.gz.

File metadata

  • Download URL: promptflip-0.2.1.tar.gz
  • Upload date:
  • Size: 73.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for promptflip-0.2.1.tar.gz
Algorithm Hash digest
SHA256 1ee7f6c69af74c895a19e3bbb6e08ca35d407a00ca9dce339a2f5bcdfbcafee7
MD5 6458b8f9f0f173009b66b8232f1b8a79
BLAKE2b-256 9d87375d10a848edd608a456047ede280c7ce42d97fb6aef9ccb1b3b021a6f2f

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptflip-0.2.1.tar.gz:

Publisher: publish.yml on tursdev-org/promptflip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file promptflip-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: promptflip-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 84.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for promptflip-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a7e5467c4a303cf334f222fd50a5106911fa9547cf1b8c00ee451395047bcd97
MD5 21b2cf544136370b89f4caf434cdc6bb
BLAKE2b-256 e41b31b00c12c3f7b7c8438f41ac77d9093d6952f3a7fafbbe55ff52b39affad

See more details on using hashes here.

Provenance

The following attestation bundles were made for promptflip-0.2.1-py3-none-any.whl:

Publisher: publish.yml on tursdev-org/promptflip

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page