Skip to main content

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 pf CLI — 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. 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"]

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 commitpf 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


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.0.tar.gz (73.5 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.0-py3-none-any.whl (84.4 kB view details)

Uploaded Python 3

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

Hashes for promptflip-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e2352492adeed275372b0c5571b7903007a7e382d72b1b4d21bdacabfbc409c1
MD5 f357ecb3e12e5d8dfe9fbe0ccba5c2cf
BLAKE2b-256 1729b090e37eb8180347edae32f581666c1b4e06381a298af66af95b58fc1a9e

See more details on using hashes here.

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

Hashes for promptflip-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3082a9e49bd6e47d463743e88f9c1258c6c128ec135acfd75e3a31382ea05461
MD5 237fe9445a5d4150ca9916e0cb9fe655
BLAKE2b-256 5f8d1466b55afd35a37969f87f63a51f980425be58345f41e3f504a70335e58f

See more details on using hashes here.

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