Skip to main content
gs_prompt_manager — one home for every prompt in your LLM app: found on disk, grouped by variant, rendered on call

gs_prompt_manager

One home for every prompt in your LLM app.
Write prompts as plain Python classes. Point the manager at the folder. Call them by name.

PyPI version Python versions License: Apache 2.0 Tests Docs Downloads

Install · 60-second tour · How it works · Prompt groups · Recipes · API · FAQ · Docs


Prompts start as one f-string in a handler. Then there is a system variant, and a terse variant for the cheap model, and a French one, and a copy someone pasted into a notebook. By the time it matters, nobody can say which string production actually sends.

gs_prompt_manager gives prompts a place to live. Each one is a small class with its template and its metadata; a directory of them becomes a namespace you can autocomplete. PromptManager("./prompts") walks the folder, imports what it finds, instantiates every PromptBase subclass, and files related variants together — so m.Reviewer.system({"language": "Rust"}) is the whole call site.

No registry file to keep in sync, no YAML, no framework. It renders strings, and strings work with every model provider.

Terminal recording: installing gs-prompt-manager, then a Python session where PromptManager discovers the Reviewer group and renders its system and verdict prompts

Why not just f-strings

f-strings in the handler gs_prompt_manager
Where a prompt lives wherever it was first needed one directory, one class each
Finding every variant grep and hope m.get_prompt_group_names()
A variable you forgot renders {name} into the API call raises before the call is made
Defaults a chain of or "" set_variable_defaults
Values the caller cannot know (dates, hostnames) threaded down through every layer <<MACRO>>, filled by the prompt itself
Version, author, tags, examples a comment, if you are lucky get_metadata()
Adding a variant new function, new import, new call site new class, nothing else

Install

pip install gs-prompt-manager

Or with uv:

uv add gs-prompt-manager

Python 3.8+. The only dependency is regex.

60-second tour

1. Write your prompts as classesprompts/reviewer.py:

from gs_prompt_manager import PromptBase, prompt_group


class ReviewerSystem(PromptBase):
    """System prompt for the code reviewer."""

    def set_prompt(self):
        return (
            "You are a senior {language} reviewer.\n"
            "Focus on correctness first, style last.\n"
            "Session started <<DATETIME>>."
        )

    def set_variable_defaults(self):
        self.variable_defaults = {"language": "Python"}


class ReviewerChat(PromptBase):
    def set_prompt(self):
        return "Review this diff:\n\n{diff}"


@prompt_group("Reviewer", "verdict")
class ReviewerFinalCall(PromptBase):
    def set_prompt(self):
        return "Answer with exactly one word: {options}."

    def set_variable_defaults(self):
        self.variable_defaults = {"options": "approve or reject"}

Three classes, one group. ReviewerSystem and ReviewerChat are filed under Reviewer by their name suffix; ReviewerFinalCall says where it belongs explicitly.

2. Point the manager at the folder:

from gs_prompt_manager import PromptManager

m = PromptManager("./prompts")

3. Call them:

>>> m.get_prompt_group_names()
['Reviewer']
>>> m.Reviewer.get_prompt_names()
['chat', 'verdict', 'system']

>>> print(m.Reviewer.system({"language": "Rust"}))
You are a senior Rust reviewer.
Focus on correctness first, style last.
Session started 2026-09-07 01:13:24.

>>> print(m.Reviewer.verdict())
Answer with exactly one word: approve or reject.

{language} came from the caller, {options} from the class default, and <<DATETIME>> from the prompt itself. Leave out a variable that has no default and you get a ValueError naming it — before a token is spent.

All of the above is in examples/; clone the repo and run python examples/quickstart.py to watch it happen.

How it works

Diagram: prompt classes in your files are discovered by PromptManager, which instantiates them and resolves them into named groups keyed by variant

Discovery is a directory walk, so the folder layout is yours to choose — nest by feature, by agent, by language, however you like. A prompt that fails to import or fails validation is logged and skipped; one broken template does not take the rest of your app down with it.

Pass a list to load several trees at once:

m = PromptManager(["./prompts", "./vendor_prompts"], verbose=True)

Pass nothing and it searches the directory of the file that constructed it.

Prompt groups

A group is one logical prompt and all its variants, addressed by key. Membership is resolved three ways, in priority order:

1. The @prompt_group decorator — explicit, wins over everything:

@prompt_group("Assistant")            # key derived from the class name -> "formal"
class AssistantFormal(PromptBase): ...

@prompt_group("Assistant", "polite")  # key stated outright -> "polite"
class SomeOtherName(PromptBase): ...

2. A recognized class-name suffixsystem, chat, pre, post, message, prompt (case-insensitive, optional underscore):

class TranslatorSystem(PromptBase): ...   # group "Translator", key "system"
class Translator_chat(PromptBase): ...    # group "Translator", key "chat"

3. Anything else becomes a solo group named after the class, key "default".

Groups read three ways, so use whichever suits the call site:

group = m.get_prompt_group("Reviewer")   # explicit
group = m.Reviewer                       # attribute

group.system({"language": "Go"})         # attribute access
group["system"]({"language": "Go"})      # dict access
group.get_prompt("system")               # explicit

"system" in group                        # True
len(group)                               # 3
list(group)                              # ['chat', 'verdict', 'system']
str(group)                               # renders default -> chat -> first member

Two prompts claiming the same key is a warning, not a crash: the first one keeps the key, and the collision is logged with both class names.

Variables and macros

Two substitutions, deliberately different, because they have different owners.

Syntax Filled by If it is missing
Variable {name} the caller, or variable_defaults ValueError
Macro <<NAME>> the prompt class, via set_macros warning, left as-is
class AuditSystem(PromptBase):
    def set_prompt(self):
        return "Audit for {user_id}. Generated <<DATETIME>> by <<HOST>>."

    def set_macros(self):
        import socket, datetime
        self.macros = {
            "<<DATETIME>>": datetime.datetime.now().isoformat(),
            "<<HOST>>": socket.gethostname(),
        }

Variables are auto-extracted from the template, so you rarely write set_variables yourself. Need a literal brace in the output — JSON in a few-shot example, say? Escape it:

def set_prompt(self):
    # {score} is a variable; the escaped braces are literal text
    return r'Reply as JSON: \{"score": {score}\}'

Use a raw string (or double the backslash) so Python itself leaves \{ alone — on 3.12+ a bare '\{' is a SyntaxWarning.

Defaults are validated against the template at import time: a default for a variable the prompt does not have raises immediately, rather than silently doing nothing.

Recipes

Anthropic — the group maps straight onto the request:

import anthropic
from gs_prompt_manager import PromptManager

m = PromptManager("./prompts")
client = anthropic.Anthropic()

reviewer = m.Reviewer
response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system=reviewer.system({"language": "Python"}),
    messages=[{"role": "user", "content": reviewer.chat({"diff": diff})}],
)

OpenAI:

messages = [
    {"role": "system", "content": m.Reviewer.system({"language": "TypeScript"})},
    {"role": "user", "content": m.Reviewer.chat({"diff": diff})},
]

One group per agent, in a multi-agent system — hand each agent its group, not a string, and every variant travels with it:

class Agent:
    def __init__(self, group):
        self.group = group

    def system_prompt(self):
        return self.group.system()

planner = Agent(m.Planner)
critic = Agent(m.Critic)

Ship a prompt catalogue — every prompt carries its own metadata, so a browsable index is one comprehension:

catalogue = {
    name: prompt.get_metadata()
    for name, prompt in m.get_prompt_instances().items()
}

Works well with

  • Emalia — an AI agent that lives in your email, plus the typed IMAP/SMTP toolkit behind it. Emalia ships its own prompts; once you start customising them, a prompt directory beats editing strings in place.
  • Anything that takes a string. Provider-agnostic on purpose: no SDK dependency, no client wrapper, no opinion about how you call your model.

API at a glance

PromptBase — subclass it, implement set_prompt, override whatever else you need.

Member What it does
set_prompt() return the template string. The one required override
set_name() defaults to the class name
set_variables() defaults to auto-extracting {...} from the template
set_variable_defaults() per-variable fallbacks
set_variable_defaults_empty() fall back to "" for anything still unset
set_macros() <<NAME>> substitutions the class computes itself
set_tools() tool identifiers this prompt expects
add_variable_default(name, value) / add_macro(key, value) tweak one entry at runtime
get_prompt(variables) / prompt(variables) render. Instances are callable
get_metadata() JSON-serializable dict: template, name, version, tags, author, example, tools

PromptManager — discovery and lookup.

Member What it does
PromptManager(prompt_paths=None, verbose=False) a str, a list of str, or None for the caller's directory
get_prompt(name) / manager.Name one prompt instance, by class name
get_prompt_group(name) / manager.Group one group. Groups win on name collisions
get_prompt_names() / get_prompt_group_names() what got discovered
get_prompt_instances() / get_prompt_groups() everything, as dicts
search_available_prompts(path) the discovery step on its own, as a static method
dir(manager) includes group and prompt names, so tab-completion works

PromptGroupgroup.key, group["key"], group.get_prompt("key"), get_prompt_names(), get_prompts(), plus in, len(), iteration and str().

FAQ

Does it call a model? No. It renders strings. Bring your own client.

Do prompts have to live in files? That is what the manager is for, but PromptBase works standalone — instantiate a subclass, or pass prompt=... to the constructor directly, and skip discovery entirely.

What happens to a prompt that raises on import? It is logged with a traceback and skipped. Everything else still loads. Construct with verbose=True to see the counts.

Can two prompts share a name? Class names must be unique across all discovered paths — a duplicate raises. Group keys only need to be unique within their group.

Is discovery slow? It imports every .py file under the given paths, once, at construction. Build the manager at startup and hold onto it; do not rebuild it per request.

Does it cope with {} in JSON examples? Yes — escape them as \{ and \}. Escaped braces are excluded from variable extraction and render as literal braces.

Documentation

Full docs at gs-prompt-manager.readthedocs.io.

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md.

License

Apache License 2.0. See LICENSE.


Built by Guan Huang · GitHub · PyPI · Issues

Release files for gs-prompt-manager 0.0.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gs-prompt-manager 0.0.10
File Size Uploaded
gs_prompt_manager-0.0.10.tar.gz 52.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gs-prompt-manager 0.0.10
File Interpreter ABI Platform
gs_prompt_manager-0.0.10-py3-none-any.whl Python 3 none any Details

Total release size: 73.3 kB

Release files / gs_prompt_manager-0.0.10.tar.gz

Download URL gs_prompt_manager-0.0.10.tar.gz
Size 52.9 kB
Tags Source
SHA-256 checksum
How to use checksums
263b149013973978fedadf429631b62c4902ebafacfa4ef04290aa042023299f
BLAKE2b-256 checksum
How to use checksums
f6c11db1fabddcc61ee1ebb0e1a3f373530162deb16a9feb1687b9150f570c97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / gs_prompt_manager-0.0.10-py3-none-any.whl

Download URL gs_prompt_manager-0.0.10-py3-none-any.whl
Size 20.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bd8b8aacd55e00a2aa6996b496c0af4923f19e5c4bcf233112962eeb59c44ec9
BLAKE2b-256 checksum
How to use checksums
382abc1d9b72905aa870a042d948dacbbae441b9e125f70917ac2ed51269465b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.10 This release

2 release files

0.0.9

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page