promptr

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 27, 2026 License: MIT Imports: 7 Imported by: 0

README

promptr

CI Go Reference

Typed prompts for Go. promptr makes a language model's output typed and reliable — the way BAML does, but as idiomatic, dependency-free Go.

Declare your prompts and their input/output types in a small .promptr schema; promptr compiles it to ordinary Go functions you call. Each generated function renders the prompt, calls a model through a tiny Provider interface you wire up (no vendor SDK in the core), and turns the loose reply into your typed value — retrying with the parse error fed back if the first attempt won't fit.

Its core is schema-aligned parsing: instead of forcing a model into a rigid JSON mode (which empirically degrades answer quality), you let it write naturally and a tolerant, schema-driven parser coerces the result into your Go types — recovering from prose preambles, Markdown fences, trailing commas, single quotes, fuzzy enum spellings, and truncated objects that real model output is full of.

.promptr schema ──promptr generate──▶ typed Go function
                                            │ renders prompt (schema baked in)
                                            ▼
                                    your Provider (any model)
                                            │ loose text reply
                                            ▼
                            coerce ──▶ typed value  (retry on misfit)

The pipeline in one example

// ticket.promptr
enum Severity { LOW HIGH CRITICAL }

class Ticket {
  title    string
  severity Severity
  tags     string[]
  due_days int?
}

client GPT4o { provider "openai" model "gpt-4o" }

function ExtractTicket(text: string) -> Ticket {
  client GPT4o
  prompt #"
    Extract a support ticket from the message.
    {{ ctx.output_schema }}      // compiler injects a schema description of Ticket
    Message: {{ text }}
  "#
}
promptr generate ./...     # -> ticket.promptr.go
ticket, err := ExtractTicket(ctx, provider, "my server is down!!")
// ticket.Severity == SeverityCRITICAL, even if the model wrote "critical priority"

The generated function bakes a human-readable schema of Ticket into the prompt (BAML's "show the model the shape you want" trick), then coerces the reply and re-asks on a parse miss — all in a few lines you can read.

coerce — the schema-aligned parser (usable on its own)

Don't want the DSL? Use the kernel directly on the structs you already have:

import "github.com/zkrebbekx/promptr/coerce"

ticket, err := coerce.Into[Ticket](modelText) // modelText: fences, prose and all

coerce.Into[T] digests input that encoding/json would reject outright:

the model emitted encoding/json coerce
```json { ... } ``` wrapped in prose ✓ extracts the payload
{ title: 'x', tags: [1,2,], } (unquoted key, single quotes, trailing comma)
{"due_days": "7"} into an int field ✓ coerces "7"7
{"amount": "$1,200.50"} into a float64 ✓ → 1200.5
{"severity": "high priority"} into an enum ✓ fuzzy-matches → HIGH
{"title": "x", "tags": ["a", (truncated) ✓ recovers what parsed

Nested structs, *T optionals, maps, snake/camel/case-insensitive keys, and a single value where a list was expected are all handled. Bare prose where a struct was expected returns a *coerce.Error — the signal the runtime retries on.

Discriminated unions
u := coerce.NewUnion(Search{}, Escalate{})
act, err := coerce.ResolveInto[Action](modelText, u) // best-fit by shape or "type" discriminator
Streaming
for p := range coerce.Stream[Ticket](tokenChan) {
    render(p.Value)        // a progressively-completed Ticket
    if p.Complete { break }
}

Prompt templates

Prompts are more than string interpolation — the template engine supports control flow over your runtime values, so one function adapts its prompt to the input:

prompt #"
  Extract a support ticket.
  {{ if examples }}Here are examples of good tickets:
  {{ for e in examples }}- {{ e }}
  {{ end }}{{ end }}
  {{ ctx.output_schema }}
  Message: {{ text }}
"#

Supported inside {{ }}: {{ var }} (with dotted paths {{ user.name }}), {{ if cond }}…{{ else }}…{{ end }} (truthiness, not, and == "lit" / != "lit"), {{ for x in items }}…{{ end }}, and the compiler-injected {{ ctx.output_schema }}. Unknown names render empty rather than erroring, so a prompt never panics on real model context. The engine (promptr.Render) is usable directly, too.

Providers

The core imports no LLM SDK. A Provider is one method:

type Provider interface {
    Complete(ctx context.Context, messages []Message) (string, error)
}
Package Backend
providers/openai OpenAI Chat Completions — and anything compatible: Azure OpenAI, Groq, Together, OpenRouter, llama.cpp/vLLM/LM Studio (just set BaseURL)
providers/anthropic Anthropic Messages API
providers/gemini Google Gemini (Generative Language API)
providers/ollama Local models via Ollama
providers/fake Deterministic scripted replies for tests and the playground

Each is net/http only — import just the one you use. Wiring any other model is a dozen lines.

Client reliability policies

Declare retry/fallback/round-robin in the DSL; the compiler generates registry-resolving constructors that wrap your wired providers:

client Fast  { provider "openai"    model "gpt-4o-mini" }
client Smart { provider "anthropic" model "claude-opus-4-8" }
client Reliable {
  fallback [Smart, Fast]   // try Smart, fall over to Fast
  retry 3                  // each up to 3 times on transient error
}
reg := promptr.Registry{"Smart": anthropicClient, "Fast": openaiClient}
provider := ClientReliable(reg) // promptr.Retry(promptr.Fallback(...), 3, 0)
ticket, err := ExtractTicket(ctx, provider, msg)

promptr.Retry, promptr.Fallback, and promptr.RoundRobin are also usable directly — each is just a Provider that wraps other Providers.

Install

# library
go get github.com/zkrebbekx/promptr

# CLI compiler
go install github.com/zkrebbekx/promptr/cmd/promptr@latest

Also published as a container image (ghcr.io/zkrebbekx/promptr) and a Homebrew cask.

Playground

A WebAssembly playground (DSL → Go, and paste-messy-output → repaired value) runs entirely client-side: playground/, deployed to GitHub Pages.

Develop

make test     # go test ./...
make race     # -race
make fuzz     # fuzz the tolerant parser
make lint
go generate ./...   # regenerate examples

License

MIT

Documentation

Overview

Package promptr is the runtime that generated .promptr code calls into: a minimal, provider-agnostic Provider interface plus the parse-with-repair loop that turns a model's loose reply into a typed Go value via the coerce kernel.

The compiler (cmd/promptr) turns each `function` in a .promptr file into a Go function whose body is a single call to Extract — so the generated code stays thin and readable, and all the retry/coercion logic lives here, tested once.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Extract

func Extract[T any](ctx context.Context, p Provider, prompt string, opts Options) (T, error)

Extract runs prompt against the provider and coerces the reply into T. If the reply will not coerce, it re-asks — appending the unparseable reply and the parse error so the model can correct itself — up to Options.Attempts times.

This is the function every generated .promptr function calls.

func Render added in v0.2.0

func Render(tmpl string, ctx map[string]any) (string, error)

Render expands a prompt template against a context map and returns the finished prompt. It is the small, dependency-free template engine the compiler targets: generated functions call Render with the call's parameters (plus a "ctx" entry carrying the baked output-schema) so prompts can use control flow over runtime values.

Supported syntax (all inside {{ }}):

{{ name }}              value lookup, with dotted paths: {{ user.name }}
{{ ctx.output_schema }} the compiler-injected schema description
{{ if cond }}…{{ end }} conditional, with optional {{ else }}
{{ for x in items }}…{{ end }}   iterate a slice, binding x in the body

A condition is a path (truthy test), `not <path>`, or `<path> == "lit"` / `<path> != "lit"`. Lookups miss softly: an unknown name renders as empty rather than erroring, so a template can never panic on real model context. Render only returns an error for a structurally broken template (an unterminated {{ if }}/{{ for }}).

Types

type Message

type Message struct {
	Role    string
	Content string
}

Message is one turn in a chat-style exchange. Roles follow the usual "system" / "user" / "assistant" convention; a Provider maps them to whatever its backend expects.

type Options

type Options struct {
	// Attempts is the maximum number of model calls (default 2): the first
	// try plus repair re-asks when the reply will not coerce into T.
	Attempts int
	// System, when non-empty, is prepended as a system message.
	System string
}

Options tunes an Extract call.

type Provider

type Provider interface {
	Complete(ctx context.Context, messages []Message) (string, error)
}

Provider is the single seam between promptr and a language model. Implement it with net/http against any chat API — the core imports no vendor SDK. A deterministic fake lives in providers/fake for tests and the playground.

func Fallback added in v0.2.0

func Fallback(providers ...Provider) Provider

Fallback wraps providers so Complete tries each in order, returning the first success; if all fail, the last error is returned. Use it to fail over from a primary model to a backup.

func Retry added in v0.2.0

func Retry(p Provider, attempts int, backoff time.Duration) Provider

Retry wraps p so that Complete is retried up to attempts times on error, sleeping backoff between tries (respecting context cancellation). attempts < 1 is treated as 1. This is reliability against transient failures, distinct from Extract's parse-repair loop (which re-asks on unparseable but successful replies).

func RoundRobin added in v0.2.0

func RoundRobin(providers ...Provider) Provider

RoundRobin wraps providers so each Complete call goes to the next provider in rotation — load-spreading across keys or endpoints. It is safe for concurrent use.

type Registry added in v0.2.0

type Registry map[string]Provider

Registry maps the client names declared in a .promptr file to live Providers the caller has wired up. Generated client constructors resolve their provider references through a Registry, so the DSL stays free of credentials.

func (Registry) Get added in v0.2.0

func (r Registry) Get(name string) Provider

Get returns the Provider registered under name, or an error Provider that fails every call — so a missing wiring surfaces at call time with a clear message rather than a nil-pointer panic.

type TemplateError added in v0.2.0

type TemplateError struct{ Msg string }

TemplateError reports a structurally invalid template.

func (*TemplateError) Error added in v0.2.0

func (e *TemplateError) Error() string

Directories

Path Synopsis
cmd
promptr command
Command promptr compiles .promptr schema files into Go.
Command promptr compiles .promptr schema files into Go.
Package codegen turns a parsed .promptr File into idiomatic Go source.
Package codegen turns a parsed .promptr File into idiomatic Go source.
Package coerce is a schema-aligned parser: it turns the loose, near-JSON text that language models actually emit into typed Go values.
Package coerce is a schema-aligned parser: it turns the loose, near-JSON text that language models actually emit into typed Go values.
Package dsl lexes and parses the .promptr schema language into an AST.
Package dsl lexes and parses the .promptr schema language into an AST.
examples
ticket
Package ticket is an end-to-end promptr example: a .promptr schema compiled to Go (ticket.promptr.go) and exercised against the fake provider in ticket_test.go.
Package ticket is an end-to-end promptr example: a .promptr schema compiled to Go (ticket.promptr.go) and exercised against the fake provider in ticket_test.go.
providers
anthropic
Package anthropic is a promptr.Provider backed by the Anthropic Messages API, built on net/http alone — no vendor SDK, in keeping with promptr's zero-SDK core.
Package anthropic is a promptr.Provider backed by the Anthropic Messages API, built on net/http alone — no vendor SDK, in keeping with promptr's zero-SDK core.
fake
Package fake is a deterministic promptr.Provider for tests, examples and the playground — no network, no API key.
Package fake is a deterministic promptr.Provider for tests, examples and the playground — no network, no API key.
gemini
Package gemini is a promptr.Provider for Google's Generative Language API (Gemini), built on net/http alone — no vendor SDK.
Package gemini is a promptr.Provider for Google's Generative Language API (Gemini), built on net/http alone — no vendor SDK.
ollama
Package ollama is a promptr.Provider for a local Ollama server, built on net/http alone — no vendor SDK.
Package ollama is a promptr.Provider for a local Ollama server, built on net/http alone — no vendor SDK.
openai
Package openai is a promptr.Provider for any OpenAI Chat Completions-compatible API, built on net/http alone — no vendor SDK.
Package openai is a promptr.Provider for any OpenAI Chat Completions-compatible API, built on net/http alone — no vendor SDK.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL