PromptFlip — the SDK that fetches prompts without your code throwing, plus the `pf` local-first authoring CLI.
Project description
PromptFlip — Python SDK + CLI
The promptflip package is two things in one install:
- the SDK — fetch managed prompts and report run telemetry, without your code ever crashing if the service is unreachable;
- the
pfCLI — a local-first, git-shaped tool for authoring prompts as files, that works fully offline and syncs to the cloud when you log in.
Install
pip install promptflip # `from promptflip import PromptFlip` + the `pf` command
Requires Python ≥3.10. One dependency: httpx.
Quickstart
from promptflip import PromptFlip
pm = PromptFlip(api_key="sk_...", environment="prod")
# Fetch a prompt. Always returns a usable Prompt — never throws.
prompt = pm.get(
"support-agent-system",
fallback="You are a helpful support agent for [[plan]]-plan users.",
inputs={"plan": user.plan},
assignment_key=user.id, # sticky A/B variant
)
# 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)
pm.send(saved_prompt_id, converted=True, revenue_usd=49)
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.
# Cloud-first with the local repo as a free/offline fallback:
pm = PromptFlip(api_key="sk_...", local_dir=".promptflip")
# Local-only — no key, no network. Resolves from the same files `pf` manages,
# rendering {{vars}} from the process environment and [[inputs]] from `inputs`:
pm = PromptFlip(local_dir=".promptflip")
# Optional cross-process cache + version pin:
pm = PromptFlip(api_key="sk_...", cache_dir="~/.cache/promptflip", cache_ttl=300)
prompt = pm.get("support-agent-system", 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"]
Command-line: local-first authoring (pf)
pf (aliased promptflip) versions prompts the way git versions code. 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.
pf config user.name "You" # set your author identity once (required before init)
pf config user.email "you@x.io"
pf init my-workspace # start a repo (no account needed — fully offline)
pf new support-agent # create prompts/support-agent.prompt.md
pf commit -m "first draft" # snapshot a version into the local store
pf status # drafts + (once synced) ahead/behind
pf log support-agent # version history
pf diff support-agent # working file vs latest committed
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
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. Free repos stay
entirely local; logging in plus pf remote upgrades the same repo to cloud sync.
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.0.tar.gz.
File metadata
- Download URL: promptflip-0.2.0.tar.gz
- Upload date:
- Size: 73.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2352492adeed275372b0c5571b7903007a7e382d72b1b4d21bdacabfbc409c1
|
|
| MD5 |
f357ecb3e12e5d8dfe9fbe0ccba5c2cf
|
|
| BLAKE2b-256 |
1729b090e37eb8180347edae32f581666c1b4e06381a298af66af95b58fc1a9e
|
File details
Details for the file promptflip-0.2.0-py3-none-any.whl.
File metadata
- Download URL: promptflip-0.2.0-py3-none-any.whl
- Upload date:
- Size: 84.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3082a9e49bd6e47d463743e88f9c1258c6c128ec135acfd75e3a31382ea05461
|
|
| MD5 |
237fe9445a5d4150ca9916e0cb9fe655
|
|
| BLAKE2b-256 |
5f8d1466b55afd35a37969f87f63a51f980425be58345f41e3f504a70335e58f
|