Metadata-Version: 2.4
Name: textprompts
Version: 2.0.0
Summary: Minimal text-based prompt-loader with TOML/YAML front-matter
Keywords: prompts,toml,yaml,frontmatter,template
Author: Jan Siml
Author-email: Jan Siml <49557684+svilupp@users.noreply.github.com>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Dist: pydantic~=2.7
Requires-Dist: pyyaml>=6.0
Requires-Dist: tomli>=1.0.0 ; python_full_version < '3.11'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/svilupp/textprompts
Project-URL: Bug Tracker, https://github.com/svilupp/textprompts/issues
Project-URL: Documentation, https://github.com/svilupp/textprompts#readme
Description-Content-Type: text/markdown

# textprompts

[![Python Docs](https://img.shields.io/badge/docs-Python-blue?style=flat&logo=python&logoColor=white)](https://siml.earth/textprompts/)
[![Julia Docs](https://img.shields.io/badge/docs-Julia-9558B2?style=flat&logo=julia&logoColor=white)](https://siml.earth/textprompts/julia/)
[![Go Docs](https://img.shields.io/badge/docs-Go-00ADD8?style=flat&logo=go&logoColor=white)](https://pkg.go.dev/github.com/svilupp/textprompts/packages/textprompts-go)
[![Hex Docs](https://img.shields.io/badge/docs-Elixir-4E2A8E?style=flat&logo=elixir&logoColor=white)](https://hexdocs.pm/textprompts)
[![PyPI version](https://img.shields.io/pypi/v/textprompts.svg)](https://pypi.org/project/textprompts/)
[![Python versions](https://img.shields.io/pypi/pyversions/textprompts.svg)](https://pypi.org/project/textprompts/)
[![CI status](https://github.com/svilupp/textprompts/workflows/CI/badge.svg)](https://github.com/svilupp/textprompts/actions)
[![Coverage](https://img.shields.io/codecov/c/github/svilupp/textprompts)](https://codecov.io/gh/svilupp/textprompts)
[![License](https://img.shields.io/pypi/l/textprompts.svg)](https://github.com/svilupp/textprompts/blob/main/LICENSE)


> **So simple, it's not even worth vibe coding yet it just makes so much sense.**

Tired of fancy UIs for prompt management that just make your system harder to debug? Prompts are text — keep them **next to your code**, let git do version control, and stop letting your formatter eat the whitespace.

## Cross-Language Support

**textprompts** uses a cross-language compatible prompt template format:

- **Python** (this package): Available on PyPI as `textprompts`
- **Node/TypeScript**: Available in `packages/textprompts-ts` folder
- **Julia**: Available in `packages/TextPrompts.jl` folder
- **Go** (alpha): Available in `packages/textprompts-go` folder ([docs](https://pkg.go.dev/github.com/svilupp/textprompts/packages/textprompts-go))
- **Elixir**: Available on Hex as `textprompts` ([package](https://hex.pm/packages/textprompts), [docs](https://hexdocs.pm/textprompts), source in `packages/textprompts-ex`)

All ports parse the shared `testdata/sections/cases.json` fixtures into the same data shape, so prompt files round-trip across your stack without conversion.

## Why textprompts?

Prompts rot. Variables get renamed, flags drift away from the branches that use them, an Agent Skill ships with a `{customer_tier}` no one passes anymore. textprompts is built so those bugs fail loudly the moment they happen — one file holds the prompt, its variables, its feature flags, and the descriptions for all of them, validated together.

- ✅ **One file, one contract** - prompt body, typed flags, variables, and their descriptions live side by side; the file *is* the spec
- ✅ **Conditional DSL, not f-string spaghetti** - `{if}` / `{switch}` branch on declared flags; unknown flags, missing variables, and uncovered enum cases raise at `format()` time
- ✅ **Catches drift over time** - rename a flag and every prompt that still references it breaks immediately instead of silently producing the wrong output
- ✅ **Just text** - git diffs your changes, formatters can't touch it, no external UI to babysit

## Installation

```bash
uv add textprompts # or pip install textprompts
```

## Quick Start

**Flexible by default** - TextPrompts loads metadata when present and still works fine without it:

1. **Create a prompt file** (`greeting.txt`):
```
---
title = "Customer Greeting"
version = "1.0.0"
description = "Friendly greeting for customer support"
---
Hello {customer_name}!

Welcome to {company_name}. We're here to help you with {issue_type}.

Best regards,
{agent_name}
```

2. **Load and use it** (no configuration needed):
```python
import textprompts

# Just load it - works with or without metadata
prompt = textprompts.load_prompt("greeting.txt")
# Or simply
alt = textprompts.Prompt.from_path("greeting.txt")

# Use it safely - all placeholders must be provided
message = prompt.prompt.format(
    customer_name="Alice",
    company_name="ACME Corp",
    issue_type="billing question",
    agent_name="Sarah"
)

print(message)

# Or use partial formatting when needed
partial = prompt.prompt.format(
    customer_name="Alice",
    company_name="ACME Corp",
    skip_validation=True
)
# Result: "Hello Alice!\n\nWelcome to ACME Corp. We're here to help you with {issue_type}.\n\nBest regards,\n{agent_name}"

# Prompt objects expose `.meta` and `.prompt`.
# Use `prompt.prompt.format()` for safe formatting or `str(prompt)` for raw text.
```

**Even simpler** - no metadata required:
```python
# simple_prompt.txt contains just: "Analyze this data: {data}"
prompt = textprompts.load_prompt("simple_prompt.txt")  # Just works!
result = prompt.prompt.format(data="sales figures")
```

## Core Features

### Safe String Formatting

Never ship a prompt with missing variables again:

```python
from textprompts import PromptString

template = PromptString("Hello {name}, your order {order_id} is {status}")

# ✅ Strict formatting - all placeholders must be provided
result = template.format(name="Alice", order_id="12345", status="shipped")

# ❌ This catches the error by default
try:
    result = template.format(name="Alice")  # Missing order_id and status
except ValueError as e:
    print(f"Error: {e}")  # Missing format variables: ['order_id', 'status']

# ✅ Partial formatting - replace only what you have
partial = template.format(name="Alice", skip_validation=True)
print(partial)  # "Hello Alice, your order {order_id} is {status}"
```

### Simple & Flexible Metadata Handling

TextPrompts is designed to be **flexible** by default - load metadata when it's present, and fall back gracefully when it isn't. No configuration needed!

```python
import textprompts

# Default behavior: load metadata if available, otherwise just use the file content
prompt = textprompts.load_prompt("my_prompt.txt")  # Just works!

# Three modes available for different use cases:
# 1. ALLOW (default): Load metadata if present, don't worry if it's incomplete
textprompts.set_metadata("allow")  # Flexible metadata loading (default)
prompt = textprompts.load_prompt("prompt.txt")  # Loads any metadata found

# 2. IGNORE: Treat as simple text file, use filename as title
textprompts.set_metadata("ignore")  # Super simple file loading
prompt = textprompts.load_prompt("prompt.txt")  # No metadata parsing
print(prompt.meta.title)  # "prompt" (from filename)

# 3. STRICT: Require complete metadata for production use
textprompts.set_metadata("strict")  # Prevent errors in production
prompt = textprompts.load_prompt("prompt.txt")  # Must have title, description, version

# Override per prompt when needed
prompt = textprompts.load_prompt("prompt.txt", meta="strict")
```

**Why this design?**
- **Default = Flexible**: Parse metadata when it's available, no friction when it's not
- **No configuration needed**: Just load files and it works
- **Production-Safe**: Use strict mode to catch missing metadata before deployment

## Real-World Examples

### OpenAI Integration

```python
import openai
from textprompts import load_prompt

system_prompt = load_prompt("prompts/customer_support_system.txt")
user_prompt = load_prompt("prompts/user_query_template.txt")

response = openai.chat.completions.create(
    model="gpt-4.1-mini",
    messages=[
        {
            "role": "system",
            "content": system_prompt.prompt.format(
                company_name="ACME Corp",
                support_level="premium"
            )
        },
        {
            "role": "user",
            "content": user_prompt.prompt.format(
                query="How do I return an item?",
                customer_tier="premium"
            )
        }
    ]
)
```

### Function Calling (Tool Definitions)

Yes, you can version control your whole tool schemas too:

```python
# tools/search_products.txt
---
title = "Product Search Tool"
version = "2.1.0"
description = "Search our product catalog"
---
{
    "type": "function",
    "function": {
        "name": "search_products",
        "description": "Search for products in our catalog",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query for products"
                },
                "category": {
                    "type": "string",
                    "enum": ["electronics", "clothing", "books"],
                    "description": "Product category to search within"
                },
                "max_results": {
                    "type": "integer",
                    "default": 10,
                    "description": "Maximum number of results to return"
                }
            },
            "required": ["query"]
        }
    }
}
```

```python
import json
from textprompts import load_prompt

# Load and parse the tool definition
tool_prompt = load_prompt("tools/search_products.txt")
tool_schema = json.loads(tool_prompt.prompt)

# Use with OpenAI
response = openai.chat.completions.create(
    model="gpt-4.1-mini",
    messages=[{"role": "user", "content": "Find me some electronics"}],
    tools=[tool_schema]
)
```

### Environment-Specific Prompts

```python
import os
from textprompts import load_prompt

env = os.getenv("ENVIRONMENT", "development")
system_prompt = load_prompt(f"prompts/{env}/system.txt")

# prompts/development/system.txt - verbose logging
# prompts/production/system.txt - concise responses
```

### Prompt Versioning & Experimentation

```python
from textprompts import load_prompt

# Easy A/B testing
prompt_version = "v2"  # or "v1", "experimental", etc.
prompt = load_prompt(f"prompts/{prompt_version}/system.txt")

# Git handles the rest:
# git checkout experiment-branch
# git diff main -- prompts/
```

## File Format

TextPrompts uses TOML front-matter (optional) followed by your prompt content.
YAML front-matter is also supported as an alternative.

**TOML (default):**
```
---
title = "My Prompt"
version = "1.0.0"
author = "Your Name"
description = "What this prompt does"
created = "2024-01-15"
tags = ["customer-support", "greeting"]
---
Your prompt content goes here.

Use {variables} for templating.
```

**YAML alternative:**
```
---
title: "My Prompt"
version: "1.0.0"
author: "Your Name"
description: "What this prompt does"
created: "2024-01-15"
tags:
  - customer-support
  - greeting
---
Your prompt content goes here.

Use {variables} for templating.
```

Both formats are auto-detected based on the front-matter content.

### Metadata Modes

Choose the right level of strictness for your use case:

1. **ALLOW** (default) - Load metadata if present, don't worry about completeness
2. **IGNORE** - Simple text file loading, filename becomes title
3. **STRICT** - Require complete metadata (title, description, version) for production safety

You can also set the environment variable `TEXTPROMPTS_METADATA_MODE` to one of
`strict`, `allow`, or `ignore` before importing the library to configure the
default mode.

```python
# Set globally
textprompts.set_metadata("allow")    # Default: load metadata when available
textprompts.set_metadata("ignore")   # Simple: no metadata parsing
textprompts.set_metadata("strict")   # Production: require complete metadata

# Or override per prompt
prompt = textprompts.load_prompt("file.txt", meta="strict")
```

## API Reference

### `load_prompt(path, *, meta=None)`

Load a single prompt file.

- `path`: Path to the prompt file
- `meta`: Metadata handling mode - `MetadataMode.STRICT`, `MetadataMode.ALLOW`, `MetadataMode.IGNORE`, or string equivalents. None uses global config.

Returns a `Prompt` object with:
- `prompt.meta`: Metadata from TOML/YAML front-matter (always present)
- `prompt.prompt`: The prompt content as a `PromptString`
- `prompt.path`: Path to the original file

### `set_metadata(mode)` / `get_metadata()`

Set or get the global metadata handling mode.

- `mode`: `MetadataMode.STRICT`, `MetadataMode.ALLOW`, `MetadataMode.IGNORE`, or string equivalents

```python
import textprompts

# Set global mode
textprompts.set_metadata(textprompts.MetadataMode.STRICT)
textprompts.set_metadata("allow")  # String also works

# Get current mode
current_mode = textprompts.get_metadata()
```

### `save_prompt(path, content, *, format="toml")`

Save a prompt to a file.

- `path`: Path to save the prompt file
- `content`: Either a string (creates template with required fields) or a `Prompt` object
- `format`: Front-matter format to use - `"toml"` (default) or `"yaml"`

```python
from textprompts import save_prompt

# Save a simple prompt with metadata template
save_prompt("my_prompt.txt", "You are a helpful assistant.")

# Save with YAML front-matter
save_prompt("my_prompt.txt", "You are a helpful assistant.", format="yaml")

# Save a Prompt object with full metadata
save_prompt("my_prompt.txt", prompt_object)
```

### `parse_sections(text)` and section utilities

Parse mixed Markdown/XML prompt structure without going through the file loader.

- `parse_sections(text)`: Returns a `ParseResult` with `sections`, `anchors`, `duplicate_anchors`, `frontmatter`, and `total_chars`
- `generate_slug(heading)`: Creates the same auto-anchor slug used by the parser (lowercase, non-alphanumeric runs -> `_`)
- `inject_anchors(text)`: Inserts missing `<a id="..."></a>` lines before Markdown headings and returns `(text, result)`
- `render_toc(result, path)`: Renders a human-readable table of contents

```python
from textprompts import inject_anchors, parse_sections, render_toc

result = parse_sections("## Intro\n\nBody.")
print(result.sections[0].anchor_id)  # "intro"

anchored_text, anchored = inject_anchors("## Intro\n\nBody.")
print(anchored_text)  # <a id="intro"></a>\n## Intro...

print(render_toc(anchored, "prompt.txt"))
```

Anchor IDs use a canonical underscore form. For example, `generate_slug("My Section")` returns `my_section`, `id="my-section"` normalizes to `my_section`, and generic XML sections default to the normalized tag name when no explicit `id` is present.

### `PromptString`

A string subclass that validates `format()` calls:

```python
from textprompts import PromptString

template = PromptString("Hello {name}, you are {role}")

# Strict formatting (default) - all placeholders required
result = template.format(name="Alice", role="admin")  # ✅ Works
result = template.format(name="Alice")  # ❌ Raises ValueError

# Partial formatting - replace only available placeholders
partial = template.format(name="Alice", skip_validation=True)  # ✅ "Hello Alice, you are {role}"

# Access placeholder information
print(template.placeholders)  # {'name', 'role'}
```

## Error Handling

TextPrompts provides specific exception types:

```python
from textprompts import (
    TextPromptsError,       # Base exception
    FileMissingError,       # File not found
    MissingMetadataError,   # No TOML front-matter when required
    InvalidMetadataError,   # Invalid TOML syntax
    MalformedHeaderError,   # Malformed front-matter structure
    MetadataMode,           # Metadata handling mode enum
    set_metadata,           # Set global metadata mode
    get_metadata            # Get global metadata mode
)
```

## CLI Tool

TextPrompts includes a CLI for quick prompt inspection:

```bash
# View a single prompt
textprompts show greeting.txt

# List all prompts in a directory
textprompts list prompts/ --recursive

# Validate prompts
textprompts validate prompts/
```

## Best Practices

1. **Organize by purpose**: Group related prompts in folders
   ```
   prompts/
   ├── customer-support/
   ├── content-generation/
   └── code-review/
   ```

2. **Use semantic versioning**: Version your prompts like code
   ```
   version = "1.2.0"  # major.minor.patch
   ```

3. **Document your variables**: List expected variables in descriptions
   ```
   description = "Requires: customer_name, issue_type, agent_name"
   ```

4. **Test your prompts**: Write unit tests for critical prompts
   ```python
   def test_greeting_prompt():
    prompt = load_prompt("greeting.txt")
    result = prompt.prompt.format(customer_name="Test")
       assert "Test" in result
   ```

5. **Use environment-specific prompts**: Different prompts for dev/prod
   ```python
   env = os.getenv("ENV", "development")
   prompt = load_prompt(f"prompts/{env}/system.txt")
   ```

## Why Not Just Use String Templates?

You could, but then you lose:
- **Metadata tracking** (versions, authors, descriptions)
- **Safe formatting** (catch missing variables)
- **Organized storage** (searchable, documentable)
- **Version control benefits** (proper diffs, blame, history)
- **Tooling support** (CLI, validation, testing)

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## License

MIT License - see [LICENSE](LICENSE) for details.

---

**textprompts** - Because your prompts deserve better than being buried in code strings. 🚀
