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
pfCLI — 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.
getalways returns aPrompt;sendalways returns. A missingapi_keyis not an error — it selects local-only mode. - Offline fallback. When the service is unreachable,
getserves the last-good cached entry (in-memory or, withcache_dir, on disk), then the local repo, then yourfallbacktemplate (with[[inputs]]filled client-side so the end-user sees the same string online or offline). - Optional disk cache. Set
cache_dirto 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
/v1HTTP contract. An import-ban test enforces zero references to private product code, and the CLI never loads onimport 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 commit → pf 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ee7f6c69af74c895a19e3bbb6e08ca35d407a00ca9dce339a2f5bcdfbcafee7
|
|
| MD5 |
6458b8f9f0f173009b66b8232f1b8a79
|
|
| BLAKE2b-256 |
9d87375d10a848edd608a456047ede280c7ce42d97fb6aef9ccb1b3b021a6f2f
|
Provenance
The following attestation bundles were made for promptflip-0.2.1.tar.gz:
Publisher:
publish.yml on tursdev-org/promptflip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptflip-0.2.1.tar.gz -
Subject digest:
1ee7f6c69af74c895a19e3bbb6e08ca35d407a00ca9dce339a2f5bcdfbcafee7 - Sigstore transparency entry: 2256995366
- Sigstore integration time:
-
Permalink:
tursdev-org/promptflip@a64065dc54b9632d6a827411395fff27817425eb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tursdev-org
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a64065dc54b9632d6a827411395fff27817425eb -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a7e5467c4a303cf334f222fd50a5106911fa9547cf1b8c00ee451395047bcd97
|
|
| MD5 |
21b2cf544136370b89f4caf434cdc6bb
|
|
| BLAKE2b-256 |
e41b31b00c12c3f7b7c8438f41ac77d9093d6952f3a7fafbbe55ff52b39affad
|
Provenance
The following attestation bundles were made for promptflip-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on tursdev-org/promptflip
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptflip-0.2.1-py3-none-any.whl -
Subject digest:
a7e5467c4a303cf334f222fd50a5106911fa9547cf1b8c00ee451395047bcd97 - Sigstore transparency entry: 2256995373
- Sigstore integration time:
-
Permalink:
tursdev-org/promptflip@a64065dc54b9632d6a827411395fff27817425eb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tursdev-org
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a64065dc54b9632d6a827411395fff27817425eb -
Trigger Event:
push
-
Statement type: