promptbuilder

package
v0.9.13 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package promptbuilder provides a safe, injection-resistant prompt construction library that leverages Go's standard encoding packages to automatically handle escaping and formatting. Similar to SQL prepared statements, but for LLM prompts.

Overview

The promptbuilder package allows developers to construct prompts with dynamic content while preventing prompt injection attacks. It achieves this by:

  • Using compile-time type safety to ensure templates come from developers
  • Automatically escaping user-provided data through standard encoders
  • Preventing transitive substitutions through single-pass tokenization
  • Immutable prompt instances - all binding methods return new instances

Basic Usage

Create a prompt template with placeholders and bind values to them:

import "chainguard.dev/driftlessaf/agents/promptbuilder"

// Templates must be literal strings (compile-time safety)
p, err := promptbuilder.NewPrompt(`
	Analyze the following data:
	{{data}}

	Instructions: {{instructions}}
`)
if err != nil {
	// Handle invalid template error
}

// Bind user data as JSON (automatically escaped)
p, err = p.BindJSON("data", userData)
if err != nil {
	// Handle binding error
}

// Bind developer-controlled literal strings
p, err = p.BindStringLiteral("instructions", "Find patterns")
if err != nil {
	// Handle binding error
}

// Build the final prompt
result, err := p.Build()
if err != nil {
	// Handle unbound placeholder error
}

Binding Methods

The package provides multiple binding methods for different data formats:

// BindStringLiteral - For developer-controlled strings only
p, err = p.BindStringLiteral("key", "literal value")

// BindJSON - Marshals data as indented JSON
p, err = p.BindJSON("data", struct{Name string}{"Alice"})

// BindXML - Marshals data as indented XML
p, err = p.BindXML("config", xmlStruct)

// BindYAML - Marshals data as YAML
p, err = p.BindYAML("settings", yamlData)

// BindUnorderedList / BindOrderedList - Render single-line items as a Markdown list
p, err = p.BindUnorderedList("steps", promptbuilder.UnorderedList{"Check logs", "Summarise failures"})

Each method also has a Must variant that panics on error:

p = p.MustBindStringLiteral("key", "value")
p = p.MustBindJSON("data", jsonData)
p = p.MustBindXML("config", xmlData)
p = p.MustBindYAML("settings", yamlData)
p = p.MustBindOrderedList("steps", promptbuilder.OrderedList{"Check logs", "Summarise failures"})

Template Syntax

Templates use {{name}} placeholders for bindings. Valid binding names must contain only letters, digits, and underscores. Invalid identifiers will cause NewPrompt to return an error.

Valid examples:

  • {{data}}
  • {{user_input}}
  • {{item1}}

Invalid examples:

  • {{}} (empty)
  • {{test-case}} (contains hyphen)
  • {{test.value}} (contains dot)

Bindable Interface

Types can implement the Bindable interface to provide custom binding logic. Executors expect request types to implement this interface so that prompts can be bound to the specific data in each request:

type Bindable interface {
	Bind(prompt *Prompt) (*Prompt, error)
}

The package provides a Noop implementation that returns the prompt unchanged, useful as an embedded field for types that conditionally bind values or when no binding is needed:

type MyRequest struct {
	promptbuilder.Noop  // Provides default Bind implementation
	Data string
}

Security Properties

  1. No Raw User Input - User data must go through encoders (XML, JSON, or YAML)
  2. Type-Safe Literals - stringLiteral ensures only developer literals bypass encoding
  3. Automatic Escaping - Encoding libraries handle all escaping for user data
  4. No Transitive Substitution - Single-pass tokenization prevents recursive replacement
  5. Immutable Prompts - All operations return new instances, preventing mutation
  6. Line-Safe Lists - List items may carry runtime data; binding rejects line breaks so an item always renders as exactly one list entry

Must Functions

For convenience in package-level variables and when templates are known to be valid:

var template = promptbuilder.MustNewPrompt(`Hello {{name}}!`)

The Must helper can wrap any (*Prompt, error) returning function:

p := promptbuilder.Must(promptbuilder.NewPrompt(literalTemplate))

Error Handling

The package returns errors for:

  • Invalid template syntax (malformed placeholders)
  • Binding to non-existent placeholders
  • Attempting to rebind already-bound placeholders
  • Building with unbound placeholders
  • Marshaling failures in BindJSON/BindXML/BindYAML
  • List items containing line breaks in BindUnorderedList/BindOrderedList
  • BindPrompt compositions nested beyond the composition depth limit
  • Builds whose rendered output exceeds the build size limit

Thread Safety

Prompt instances are immutable after creation. Binding methods return new instances, making the original safe to share across goroutines. However, concurrent calls to Build() on the same instance are safe only because the instance is immutable.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Bindable

type Bindable interface {
	// Bind takes a prompt and returns a new prompt with bound values.
	// The implementation should bind any necessary values from the receiver to the prompt.
	// This allows executors to pass request-specific data into prompt templates.
	Bind(prompt *Prompt) (*Prompt, error)
}

Bindable represents a type that can bind values to a Prompt. Executors expect request types to implement this interface so that prompts can be bound to the specific data in each request.

type Noop

type Noop struct{}

Noop is a no-op implementation of Bindable that passes through the prompt unchanged

func (Noop) Bind

func (Noop) Bind(prompt *Prompt) (*Prompt, error)

Bind implements Bindable by returning the prompt unchanged

type OrderedList added in v0.7.64

type OrderedList []string

OrderedList is a list of single-line text items rendered as a numbered Markdown list. It is a distinct type from UnorderedList, so a list declared as one cannot be bound as the other; plain []string values convert to either.

type Prompt

type Prompt struct {
	// contains filtered or unexported fields
}

Prompt represents a template with bindable placeholders

func Must

func Must(p *Prompt, err error) *Prompt

Must is a helper that wraps a call to a function returning (*Prompt, error) and panics if the error is non-nil. It is intended for use in variable initializations such as:

var p = promptbuilder.Must(promptbuilder.NewPrompt(`Hello {{name}}`))

func MustNewPrompt

func MustNewPrompt(template stringLiteral) *Prompt

MustNewPrompt creates a new prompt from a template literal and panics on error. This is syntactic sugar for Must(NewPrompt(...))

Example

ExampleMustNewPrompt demonstrates creating a prompt that panics on error

package main

import (
	"fmt"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	// This is safe for package-level variables with known-good templates
	var template = promptbuilder.MustNewPrompt(`Analyze: {{data}}`)

	bindings := template.GetBindings()
	fmt.Printf("Template has %d binding\n", len(bindings))
}
Output:
Template has 1 binding

func NewPrompt

func NewPrompt(template stringLiteral) (*Prompt, error)

NewPrompt creates a new prompt from a template literal and parses bindings

Example

ExampleNewPrompt demonstrates creating a new prompt template

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	p, err := promptbuilder.NewPrompt(`Hello {{name}}, welcome to {{service}}!`)
	if err != nil {
		log.Fatal(err)
	}

	bindings := p.GetBindings()
	fmt.Printf("Found %d bindings\n", len(bindings))
}
Output:
Found 2 bindings

func (*Prompt) BindJSON

func (p *Prompt) BindJSON(name string, data any) (*Prompt, error)

BindJSON binds structured data to a placeholder by marshaling it as JSON The data parameter can be any type that json.Marshal accepts Returns a new Prompt with the binding applied

Example

ExamplePrompt_BindJSON demonstrates binding structured data as JSON

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	p := promptbuilder.MustNewPrompt(`Process this user data:
{{user_data}}`)

	userData := map[string]any{
		"name": "Alice",
		"age":  30,
		"tags": []string{"developer", "go"},
	}

	p, err := p.BindJSON("user_data", userData)
	if err != nil {
		log.Fatal(err)
	}

	result, err := p.Build()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}
Output:
Process this user data:
{
  "age": 30,
  "name": "Alice",
  "tags": [
    "developer",
    "go"
  ]
}

func (*Prompt) BindOrderedList added in v0.7.64

func (p *Prompt) BindOrderedList(name string, items OrderedList) (*Prompt, error)

BindOrderedList binds items as a numbered Markdown list. Items may carry runtime data but must be single-line: an item containing a line break is rejected, so every item renders as exactly one list entry.

func (*Prompt) BindPrompt added in v0.7.64

func (p *Prompt) BindPrompt(name string, other *Prompt) (*Prompt, error)

BindPrompt binds another Prompt's Build output to a placeholder. The inner Prompt is built each time the outer one is, so unresolved bindings inside inner surface as Build errors on the outer. Use this to compose Prompts when the content at a placeholder is itself produced by a (literal-built) Prompt.

func (*Prompt) BindStringLiteral

func (p *Prompt) BindStringLiteral(name string, value stringLiteral) (*Prompt, error)

BindStringLiteral binds a literal string value to a placeholder The value comes from the developer, not from user input Returns a new Prompt with the binding applied

Example

ExamplePrompt_BindStringLiteral demonstrates binding literal string values

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	p := promptbuilder.MustNewPrompt(`System: {{instructions}}
User: {{query}}`)

	// Bind developer-provided literal strings
	p, err := p.BindStringLiteral("instructions", "You are a helpful assistant.")
	if err != nil {
		log.Fatal(err)
	}

	p, err = p.BindStringLiteral("query", "What is the weather?")
	if err != nil {
		log.Fatal(err)
	}

	result, err := p.Build()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}
Output:
System: You are a helpful assistant.
User: What is the weather?

func (*Prompt) BindUnorderedList added in v0.7.64

func (p *Prompt) BindUnorderedList(name string, items UnorderedList) (*Prompt, error)

BindUnorderedList binds items as an unordered Markdown list. Items may carry runtime data but must be single-line: an item containing a line break is rejected, so every item renders as exactly one bullet.

func (*Prompt) BindXML

func (p *Prompt) BindXML(name string, data any) (*Prompt, error)

BindXML binds structured data to a placeholder by marshaling it as XML The data parameter can be any type that xml.Marshal accepts Returns a new Prompt with the binding applied

Example

ExamplePrompt_BindXML demonstrates binding structured data as XML

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	type User struct {
		Name string `xml:"name"`
		Age  int    `xml:"age"`
	}

	p := promptbuilder.MustNewPrompt(`User profile:
{{profile}}`)

	user := User{Name: "Bob", Age: 25}

	p, err := p.BindXML("profile", user)
	if err != nil {
		log.Fatal(err)
	}

	result, err := p.Build()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}
Output:
User profile:
<User>
  <name>Bob</name>
  <age>25</age>
</User>

func (*Prompt) BindYAML

func (p *Prompt) BindYAML(name string, data any) (*Prompt, error)

BindYAML binds structured data to a placeholder by marshaling it as YAML The data parameter can be any type that yaml.Marshal accepts Returns a new Prompt with the binding applied

Example

ExamplePrompt_BindYAML demonstrates binding structured data as YAML

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	p := promptbuilder.MustNewPrompt(`Configuration:
{{config}}`)

	config := map[string]any{
		"database": map[string]string{
			"host": "localhost",
			"port": "5432",
		},
		"debug": true,
	}

	p, err := p.BindYAML("config", config)
	if err != nil {
		log.Fatal(err)
	}

	result, err := p.Build()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}
Output:
Configuration:
database:
    host: localhost
    port: "5432"
debug: true

func (*Prompt) Build

func (p *Prompt) Build() (string, error)

Build constructs the final prompt, returning an error if any bindings are unbound or the rendered output exceeds the build size limit.

func (*Prompt) GetBindings

func (p *Prompt) GetBindings() map[string]struct{}

GetBindings returns the names of all bindings found in the template as a set This is useful for testing and debugging

func (*Prompt) MustBindJSON

func (p *Prompt) MustBindJSON(name string, data any) *Prompt

MustBindJSON binds structured data as JSON to a placeholder and panics on error. This is syntactic sugar for Must(p.BindJSON(...))

func (*Prompt) MustBindOrderedList added in v0.7.64

func (p *Prompt) MustBindOrderedList(name string, items OrderedList) *Prompt

MustBindOrderedList binds single-line items as a numbered Markdown list and panics on error.

func (*Prompt) MustBindPrompt added in v0.7.64

func (p *Prompt) MustBindPrompt(name string, other *Prompt) *Prompt

MustBindPrompt binds another Prompt's Build output to a placeholder and panics on error.

func (*Prompt) MustBindStringLiteral

func (p *Prompt) MustBindStringLiteral(name string, value stringLiteral) *Prompt

MustBindStringLiteral binds a literal string value to a placeholder and panics on error. This is syntactic sugar for Must(p.BindStringLiteral(...))

Example

ExamplePrompt_MustBindStringLiteral demonstrates the Must variant for binding literals

package main

import (
	"fmt"
	"log"

	"chainguard.dev/driftlessaf/agents/promptbuilder"
)

func main() {
	p := promptbuilder.MustNewPrompt(`Hello {{name}}!`)

	// Chain Must methods for fluent API when you know bindings will succeed
	p = p.MustBindStringLiteral("name", "World")

	result, err := p.Build()
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(result)
}
Output:
Hello World!

func (*Prompt) MustBindUnorderedList added in v0.7.64

func (p *Prompt) MustBindUnorderedList(name string, items UnorderedList) *Prompt

MustBindUnorderedList binds single-line items as an unordered Markdown list and panics on error.

func (*Prompt) MustBindXML

func (p *Prompt) MustBindXML(name string, data any) *Prompt

MustBindXML binds structured data as XML to a placeholder and panics on error. This is syntactic sugar for Must(p.BindXML(...))

func (*Prompt) MustBindYAML

func (p *Prompt) MustBindYAML(name string, data any) *Prompt

MustBindYAML binds structured data as YAML to a placeholder and panics on error. This is syntactic sugar for Must(p.BindYAML(...))

type UnorderedList added in v0.7.64

type UnorderedList []string

UnorderedList is a list of single-line text items rendered as Markdown bullets. It is a distinct type from OrderedList, so a list declared as one cannot be bound as the other; plain []string values convert to either.

Jump to

Keyboard shortcuts

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