Metadata-Version: 2.4
Name: promptree
Version: 0.3.0
Summary: Type-safe Jinja prompt trees with runtime validation and generated Python stubs.
Requires-Python: >=3.10
Requires-Dist: jinja2>=3.0.0
Provides-Extra: watch
Requires-Dist: watchdog>=3.0; extra == 'watch'
Description-Content-Type: text/markdown

# promptree

`promptree` turns a directory of Jinja templates into a type-safe Python prompt tree, with runtime validation and generated `.pyi` stubs for IDE autocomplete.

It gives you:

- Dot-notation access to prompt folders and files
- Runtime validation through Jinja `StrictUndefined`
- Generated `.pyi` stubs for IDE autocomplete
- Full Jinja support, including `include`, `extends`, and macros
- Raw strings as the only integration format, so there is no adapter layer

## Install

```bash
pip install promptree
```

Optional extras:

```bash
pip install promptree[watch]
pip install promptree[pydantic-ai]
```

## Quick Start

Directory layout:

```text
prompts/
├── system.md
└── user/
    ├── greeting.md
    └── farewell.txt
```

Example templates:

```jinja
{# prompts/system.md #}
System prompt for {{ name }}.
```

```jinja
{# prompts/user/greeting.md #}
Hello {{ name }}!
```

```jinja
{# prompts/user/farewell.txt #}
Goodbye {{ name }}.
```

Use them from Python:

```python
from promptree import Promptree

prompts = Promptree("./prompts")

print(prompts.system(name="Claude"))
print(prompts.user.greeting(name="Tim"))
print(prompts.user.farewell(name="Tim"))
```

This direct `Promptree(...)` usage is runtime-dynamic. Editors can execute the code
correctly, but they cannot infer from the filesystem whether `prompts.system` is a
directory node or a template file. For precise VS Code autocomplete and call signatures,
generate the prompt package and import its `tree` object:

```bash
promptree generate ./prompts
```

```python
from prompts import tree

print(tree.system(name="Claude"))
print(tree.user.greeting(name="Tim"))
```

The CLI can generate an importable package and matching type stubs inside the prompt directory:

```bash
promptree generate ./prompts
promptree check ./prompts
promptree --version
```

For local VS Code navigation with absolute file links:

```bash
promptree generate ./prompts --stub-link-mode file-uri
```

You can also run the CLI as a module:

```bash
python -m promptree generate ./prompts
```

## Pre-commit And CI

`promptree check` regenerates the stubs in memory and exits non-zero if the files on disk are stale.
That makes it useful for both local pre-commit enforcement and CI.

If you keep a hand-written `__init__.py` in a prompt directory, `promptree generate` will leave it alone and `promptree check` will ignore that file.

The repository includes a hook definition in [`.pre-commit-hooks.yaml`](./.pre-commit-hooks.yaml), and a consumer can wire it up like this:

```yaml
- repo: https://github.com/your-org/promptree
  rev: v0.1.0
  hooks:
    - id: promptree-check
      files: ^prompts/
```

For CI, run the same command directly:

```bash
promptree check ./prompts
```

## Why No Adapter Layer

Rendered prompt text is the common format across libraries like pydantic_ai, LangChain, and the OpenAI Python client.
`promptree` focuses on generating and validating that text well, rather than wrapping every downstream API.

If you want IDE type safety, prefer importing the generated prompt package over constructing
`Promptree(...)` inline in application code.

## pydantic_ai

Full runnable example:

```python
from dataclasses import dataclass

from pydantic_ai import Agent, RunContext
from prompts import tree as prompts


@dataclass
class MyDeps:
    user_name: str
    language: str


agent = Agent("openai:gpt-4o", deps_type=MyDeps)

# Static: render once at agent creation
agent_static = Agent(
    "openai:gpt-4o",
    instructions=prompts.system(name="Claude"),
)


# Dynamic: re-evaluated every run with access to ctx.deps
@agent.instructions
def dynamic_instructions(ctx: RunContext[MyDeps]) -> str:
    return prompts.system(
        name=ctx.deps.user_name,
        language=ctx.deps.language,
    )


result = agent.run_sync(
    prompts.user.greeting(name="Tim"),
    deps=MyDeps(user_name="Tim", language="en"),
)
print(result.output)
```

## LangChain

```python
from langchain_core.messages import HumanMessage, SystemMessage

from promptree import Promptree

prompts = Promptree("./prompts")

messages = [
    SystemMessage(content=prompts.system(name="Claude")),
    HumanMessage(content=prompts.user.greeting(name="Tim")),
]
```

## Raw OpenAI Client

```python
from openai import OpenAI

from promptree import Promptree

client = OpenAI()
prompts = Promptree("./prompts")

client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": prompts.system(name="Claude")},
        {"role": "user", "content": prompts.user.greeting(name="Tim")},
    ],
)
```

## Generated Package

When you run `promptree generate ./prompts`, promptree writes two files into the prompt directory:

- `__init__.py` with a `tree` object backed by `Promptree`
- `__init__.pyi` with nested classes and typed call signatures for autocomplete

Generated template stubs also embed:

- a Markdown source link
- the first 10 non-empty lines of the underlying template text

This does not force VS Code to jump directly into the `.md` or `.jinja` file on
`Go to Definition`, but it does make the generated symbol carry a useful source reference
and prompt excerpt in the stub itself.

Stub source links support two modes:

- `relative` (default): emits links like `[Source](./src/docmist/prompts/...)`, suitable for
  portable, committable generated files
- `file-uri`: emits links like `[Source](file:///...)`, useful when VS Code only treats
  absolute local file links as clickable

Configure this with `--stub-link-mode relative` or `--stub-link-mode file-uri`.

You can tune the excerpt length with `--stub-source-lines N` on both `generate` and `check`.

That means you can import the generated package and get both runtime access and IDE support from the same directory.

## Deployment Context

Templates can declare deployment-specific text through direct `context.<field>` access:

```jinja
{% if context.region_hints %}
{{ context.region_hints }}
{% endif %}
```

Context is scoped to each callable prompt and follows the prompt directory structure:

```json
{
  "weather": {
    "region_hints": "Use local warning information."
  },
  "documents": {
    "summary": {
      "storage_hints": "Internal documents are stored under /internal."
    }
  }
}
```

Configure exactly one source:

```python
prompts = Promptree("./prompts", context={...})
prompts = Promptree("./prompts", context_file="./context.json")
prompts = Promptree("./prompts", context_env="PROMPTREE_CONTEXT")
```

Missing prompt entries and fields default to empty strings, so an empty object is a valid entry for
a prompt that declares fields. Prompts without declared fields must not appear in the file at all.
Unknown prompts, unknown fields, non-string values, and invalid nesting raise `PromptContextError`.
Context values are rendered as literal text and are never evaluated as Jinja source.

Create and validate context files with:

```bash
promptree context init ./prompts --output context.json
promptree context check ./prompts context.json
```

`context init` writes a skeleton holding every prompt that declares at least one field, with all
values empty, and the result always passes `context check`. That makes the generated file a
machine-readable description of the context interface: its diff between two releases tells your
deployments what changed. Adding a field stays backwards compatible because missing fields render
as empty strings.

### When templates are analyzed

Without a context source, `Promptree(...)` stays lazy and parses nothing until a prompt is called.
As soon as `context`, `context_file`, or `context_env` is passed, the constructor analyzes the whole
tree up front — even when the environment variable is unset. Template violations should not surface
only in deployments that happen to mount a context file.

### Dependencies

Static includes and inheritance automatically contribute their fields to the calling prompt.
Includes without context and imports without an explicit `with context` are rejected when their
target needs deployment context. Underscore directories such as `_partials/` stay absent from the
public prompt tree but can provide shared static dependencies, and their fields count towards every
prompt that includes them.

Dot-prefixed files and directories are skipped, so a stray `.backup/old.md` cannot break generation.
A template that includes such a file explicitly still pulls it into the analysis.

A prompt whose dependency chain contains a dynamic template reference cannot be resolved statically:

```jinja
{% include template_name %}
```

That prompt is excluded from the context mechanism and declares no fields. Using context fields in
the same chain is an error naming the file and line of the dynamic reference. Only that one prompt
is affected; the rest of the tree keeps using context normally.

### Errors

`PromptContextError` reports violated context rules: invalid data, misuse of the reserved `context`
name, and dependencies that cannot forward context. `TemplateAnalysisError` reports templates that
fail to parse at all. Both derive from `PromptreeError`, and the CLI prints them without a
traceback.

### Generated packages

Generated packages can opt into environment loading:

```bash
promptree generate ./prompts --context-env PROMPTREE_CONTEXT
promptree check ./prompts --context-env PROMPTREE_CONTEXT
```

Each generated stub documents the fields its prompt uses:

```python
class _WeatherNode:
    def __call__(self, *, name: Any) -> str:
        """[Source](./weather.md)

        Context fields: region_hints, style_hints

        Hello {{ name }}
        {% if context.region_hints %}{{ context.region_hints }}{% endif %}
        """
        ...
```

`context` never appears in `TemplateCallable.variables` or in generated call signatures — it is
supplied by the tree, not by the caller.
