Documentation
¶
Overview ¶
Package promptloom provides a modular prompt management system for LLM applications.
PromptLoom allows you to define reusable prompt components, organize them into profiles with inheritance, and assemble complete prompts based on runtime context.
Core Concepts ¶
Components are reusable prompt fragments defined in YAML:
name: greeting description: A friendly greeting component instructions: | Hello! I'm here to help you today.
Profiles combine components and can inherit from other profiles:
name: assistant inherits: base components: - greeting - instructions - formatting
Context provides runtime values for conditional assembly:
ctx := promptloom.NewContext().
SetFlag("has_agent", true).
SetValue("user_name", "Alice").
SetNumber("max_tokens", 4096)
Basic Usage ¶
Load components and profiles from a filesystem:
registry := promptloom.NewRegistry()
store := storage.NewEmbedStorage(promptsFS, "prompts")
if err := registry.LoadFromFS(store.FS(), store.RootPath()); err != nil {
log.Fatal(err)
}
Assemble a prompt:
assembler := promptloom.NewAssembler(registry)
ctx := promptloom.NewContext().SetFlag("verbose", true)
prompt, err := assembler.Assemble("my-profile", ctx)
Conditional Components ¶
Components can have conditions that determine when they're included:
name: agent_mode condition: has_agent instructions: | You have access to tools and can take actions.
Conditions support boolean logic, comparisons, and advanced operators:
Boolean operators:
- Simple flags: "has_agent"
- Negation: "!has_agent"
- AND: "has_agent && is_premium"
- OR: "has_agent || is_demo"
- Parentheses: "(a || b) && c" for grouping
Comparison operators:
- String equality: "tier == 'premium'", "model != 'gpt-3'"
- Numeric: "count > 10", "limit <= 100", "value == 42"
Membership operator:
- In arrays: "tier in ['free', 'premium', 'enterprise']"
- Numeric arrays: "priority in [1, 2, 3]"
Existence checks:
- Exists: "tier exists" (true if variable is set, even if empty)
- Not exists: "legacy_field !exists"
Dot notation for nested Data access:
- Simple: "user.role == 'admin'"
- Deep: "config.features.beta exists"
- With operators: "user.role in ['admin', 'mod']"
Ordered Sections with Conditions and Priorities ¶
Components can have ordered sections with individual conditions and priorities:
name: features
instructions: Base instructions.
ordered_sections:
- name: safety
priority: 100
content: Safety guidelines (always first).
- name: premium
priority: 50
condition: is_premium
content: Premium features enabled.
- name: basic
priority: 25
content: Basic features.
Sections are sorted by priority (highest first) and can have their own condition expressions using the same syntax as component conditions. Sections with equal priority maintain their definition order. Sections inherit both the component's role and template setting.
Component Dependencies ¶
Components can declare dependencies on other components using depends_on:
name: advanced_features depends_on: base_features # Single dependency instructions: | Advanced features that require base features. name: premium_advanced depends_on: [base, premium] # Multiple dependencies (all required) instructions: | Premium advanced features.
If any dependency was not included (due to condition evaluation or not being in the profile), the dependent component is automatically skipped. Dependencies must appear earlier in the profile's component list. This naturally prevents circular dependencies.
Template Variables ¶
Enable template processing to inject Context values into prompts:
name: personalized
template: true
instructions: |
Hello {{.Values.user_name}}! You have {{.Numbers.message_count}} messages.
{{if .Flags.premium}}Premium features are enabled.{{end}}
Available template variables:
- {{.Flags.name}} - boolean flags
- {{.Values.name}} - string values
- {{.Numbers.name}} - integer values
- {{.Data.name}} - arbitrary data
- {{.Component.Name}} - current component name
- {{.Component.Data.key}} - component's custom data
Profile Inheritance ¶
Profiles can inherit from parent profiles:
name: claude-sonnet
inherits: claude
overrides:
preferred_keywords:
- "step by step"
Child profiles inherit components and overrides from parents, with child values taking precedence.
Provider-Aware Profiles ¶
Profiles can be organized by LLM provider using a naming convention. Provider-specific variants use the pattern "{base}.{provider}":
# profiles/assistant.yaml - universal base name: assistant components: - base_instructions - safety_rules # profiles/assistant.anthropic.yaml - Anthropic variant name: assistant.anthropic inherits: assistant # REQUIRED for provider variants provider: anthropic components: - anthropic_persona
When assembling with a provider set in context, the assembler automatically resolves to the provider-specific variant if it exists:
ctx := promptloom.NewContext().SetProvider("anthropic")
messages, _ := assembler.AssembleMessages("assistant", ctx)
// Uses "assistant.anthropic" because it exists
ctx2 := promptloom.NewContext().SetProvider("unknown")
messages2, _ := assembler.AssembleMessages("assistant", ctx2)
// Falls back to "assistant" because "assistant.unknown" doesn't exist
Provider variant profiles MUST inherit from their base profile. This is validated at registry validation time. The naming convention ensures that updating the base profile automatically affects all variants through inheritance.
Helper methods for querying provider variants:
providers := registry.GetProviderVariants("assistant")
// Returns []string{"anthropic", "openai"} if those variants exist
exists := registry.HasProviderVariant("assistant", "anthropic")
// Returns true if assistant.anthropic exists and inherits from assistant
Validation ¶
Validate the registry to catch configuration errors:
if err := registry.Validate(); err != nil {
log.Fatal(err) // Catches circular inheritance
}
// Strict mode also checks for unknown component references
if err := registry.ValidateStrict(); err != nil {
log.Fatal(err)
}
Debugging ¶
Use AssembleWithTrace to debug complex profiles and access profile metadata:
prompt, trace, err := assembler.AssembleWithTrace("profile", ctx)
fmt.Println("Included:", trace.IncludedComponents)
fmt.Println("Skipped:", trace.SkippedComponents)
fmt.Println("Conditions:", trace.ConditionResults)
// Access resolved profile with overrides and metadata
profile := trace.ResolvedProfile
keywords := profile.Overrides.PreferredKeywords
modelID := profile.GetDataString("model_id")
Message-Based Assembly ¶
For LLM APIs that expect structured messages (like OpenAI, Anthropic), use AssembleMessages to get a slice of Message structs instead of a single string:
messages, err := assembler.AssembleMessages("profile", ctx)
// messages is []Message with Role and Content fields
Components can specify a role (system, user, assistant):
name: user_input role: user instructions: | What is the weather today?
If no role is specified, components default to "system". Adjacent components with the same role are automatically merged into a single message:
// Two system components back-to-back become one system message
messages, _ := assembler.AssembleMessages("profile", ctx)
// messages[0].Role == "system"
// messages[0].Content == "First component.\n\nSecond component."
ModelInstructions and Overrides.Suffix are appended to the last system message. If no system message exists (all components are user/assistant roles), these values are skipped as they are supplementary to system content.
Provider Formatters ¶
Different LLM APIs have different message formats. Optional sub-packages under format/ convert PromptLoom messages to provider-specific formats:
import "github.com/lirancohen/promptloom/format/openai"
import "github.com/lirancohen/promptloom/format/anthropic"
messages, _ := assembler.AssembleMessages("profile", ctx)
// Convert to OpenAI format (keeps system messages in array)
openaiMsgs := openai.Format(messages)
// Convert to Anthropic format (extracts system to top-level field)
anthropicReq := anthropic.Format(messages)
OpenAI's format keeps system messages in the messages array, while Anthropic requires system messages as a separate top-level parameter. The formatters handle these differences automatically.
Users can implement the format.Formatter interface for custom providers.
Prompt Chaining ¶
For multi-step workflows where one LLM response feeds into the next prompt, reuse the same Context object and set intermediate results as values:
ctx := promptloom.NewContext()
// Step 1: Analyze
messages1, _ := assembler.AssembleMessages("analyze", ctx)
response1 := callLLM(messages1)
// Step 2: Use analysis in next prompt
ctx.SetValue("analysis", response1)
messages2, _ := assembler.AssembleMessages("synthesize", ctx)
response2 := callLLM(messages2)
The "synthesize" profile uses a template component to inject the previous response:
name: synthesize_prompt
template: true
instructions: |
Based on this analysis:
{{.Values.analysis}}
Please synthesize the key findings.
This pattern keeps orchestration logic in your application code while PromptLoom handles prompt composition. You can chain any number of steps by continuing to set values and assemble new prompts.
Assembly Behavior ¶
Assemble fails fast on missing components and validates inputs automatically:
prompt, err := assembler.Assemble("profile", ctx)
if err != nil {
log.Fatal(err) // Fails if component missing or required inputs missing
}
If a profile has an InputSchema, defaults are applied and required inputs are validated automatically during assembly. No separate validation call needed.
Profile Metadata ¶
Profiles can store arbitrary metadata with type-safe accessors:
name: my-profile
metadata:
model_id: "gpt-4"
max_tokens: 4096
preferred_keywords: ["helpful", "concise"]
profile := registry.GetProfile("my-profile")
modelID := profile.GetDataString("model_id")
maxTokens := profile.GetDataInt("max_tokens")
keywords := profile.GetDataStrings("preferred_keywords")
Version and Tags ¶
Profiles can have versions and tags for organization and inheritance:
name: assistant-v2 version: "2.0.0" tags: - production - vision - premium
Tags are inherited when a profile inherits from another. Access tags directly via profile.Tags or use slices.Contains for membership checks.
Input Schema ¶
Profiles can declare expected inputs for documentation and validation:
name: my-profile
input_schema:
inputs:
- name: user_name
type: string
required: true
description: The user's display name
- name: is_premium
type: flag
default: false
When a profile has an InputSchema, Assemble automatically:
- Applies default values to the context
- Validates required inputs are present
- Checks input types match the schema
If validation fails, Assemble returns an error. No separate validation call needed.
Input types: "flag" (bool), "string", "number" (int), "data" (any)
Configurable Directory Structure ¶
By default, LoadFromFS expects "components" and "profiles" directories. Customize these with options:
registry.LoadFromFS(fsys, rootPath,
WithComponentsDir("snippets"),
WithProfilesDir("configs"),
)
Storage Backends ¶
PromptLoom supports multiple storage backends:
- storage.NewMemoryStorage() - In-memory storage for testing
- storage.NewEmbedStorage(fs, path) - Embedded files (go:embed)
Thread Safety ¶
Registry and Assembler are safe for concurrent use. The Assembler caches resolved profiles for performance; use ClearCache() if you modify profiles after initial assembly.
Example ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
// Create a memory storage for this example
store := storage.NewMemoryStorage()
store.AddComponent("greeting", `
name: greeting
instructions: Hello! I'm your AI assistant.
`)
store.AddComponent("task", `
name: task
instructions: How can I help you today?
`)
store.AddProfile("assistant", `
name: assistant
components:
- greeting
- task
`)
// Load into registry
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
// Assemble the prompt
assembler := promptloom.NewAssembler(registry)
ctx := promptloom.NewContext()
prompt, _ := assembler.Assemble("assistant", ctx)
fmt.Println(prompt)
}
Output: Hello! I'm your AI assistant. How can I help you today?
Index ¶
- Constants
- type Assembler
- func (a *Assembler) Assemble(profileName string, ctx *Context) (string, error)
- func (a *Assembler) AssembleMessages(profileName string, ctx *Context) ([]Message, error)
- func (a *Assembler) AssembleMessagesWithTrace(profileName string, ctx *Context) ([]Message, *AssemblyTrace, error)
- func (a *Assembler) AssembleWithTrace(profileName string, ctx *Context) (string, *AssemblyTrace, error)
- func (a *Assembler) ClearCache()
- func (a *Assembler) GetAvoidKeywords(profileName string) []string
- func (a *Assembler) GetInputSchema(profileName string) *InputSchema
- func (a *Assembler) GetOverrides(profileName string) *ProfileOverrides
- func (a *Assembler) GetPreferredKeywords(profileName string) []string
- type AssemblyTrace
- type Component
- func (c *Component) GetData(key string) any
- func (c *Component) GetDataBool(key string) bool
- func (c *Component) GetDataInt(key string) int
- func (c *Component) GetDataString(key string) string
- func (c *Component) GetDataStrings(key string) []string
- func (c *Component) GetDependencies() []string
- func (c *Component) GetRole() string
- type ComponentData
- type ConditionError
- type Context
- func (c *Context) GetData(name string) any
- func (c *Context) GetFlag(name string) bool
- func (c *Context) GetNestedValue(path string) (any, bool)
- func (c *Context) GetNumber(name string) int
- func (c *Context) GetProvider() string
- func (c *Context) GetValue(name string) string
- func (c *Context) HasData(name string) bool
- func (c *Context) HasFlag(name string) bool
- func (c *Context) HasNumber(name string) bool
- func (c *Context) HasValue(name string) bool
- func (c *Context) SetData(name string, value any) *Context
- func (c *Context) SetFlag(name string, value bool) *Context
- func (c *Context) SetFlags(flags map[string]bool) *Context
- func (c *Context) SetNumber(name string, value int) *Context
- func (c *Context) SetNumbers(numbers map[string]int) *Context
- func (c *Context) SetProvider(provider string) *Context
- func (c *Context) SetValue(name, value string) *Context
- func (c *Context) SetValues(values map[string]string) *Context
- type IncludedSection
- type InputField
- type InputSchema
- type LoadOption
- type Message
- type Profile
- type ProfileOverrides
- type Registry
- func (r *Registry) GetComponent(name string) *Component
- func (r *Registry) GetProfile(name string) *Profile
- func (r *Registry) GetProfilesByTag(tag string) []*Profile
- func (r *Registry) GetProviderVariants(baseName string) []string
- func (r *Registry) HasProviderVariant(baseName, provider string) bool
- func (r *Registry) ListComponents() []string
- func (r *Registry) ListProfiles() []string
- func (r *Registry) LoadFromFS(fsys fs.FS, rootPath string, opts ...LoadOption) error
- func (r *Registry) RegisterComponent(c *Component)
- func (r *Registry) RegisterProfile(p *Profile)
- func (r *Registry) Validate() error
- func (r *Registry) ValidateConditions(ctx *Context) []error
- func (r *Registry) ValidateStrict() error
- func (r *Registry) ValidateWithOptions(opts ValidateOptions) error
- type Section
- type SkippedComponent
- type SkippedSection
- type StringOrSlice
- type TemplateData
- type ValidateOptions
- type ValidationError
Examples ¶
- Package
- Assembler.Assemble (Conditional)
- Assembler.Assemble (EnhancedConditions)
- Assembler.Assemble (MissingComponent)
- Assembler.AssembleWithTrace (ConditionalComponents)
- Assembler.AssembleWithTrace (WithMetadata)
- Component (OrderedSections)
- Component (OrderedSections_priority)
- Component (Template)
- Context
- Context (Batch_setters)
- InputSchema
- Profile (Inheritance)
- Profile.GetDataString
- Registry.LoadFromFS (CustomDirs)
- Registry.Validate
Constants ¶
const ( InputTypeFlag = "flag" InputTypeString = "string" InputTypeNumber = "number" InputTypeData = "data" )
InputType constants for input field types.
const RoleAssistant = "assistant"
RoleAssistant is the role for assistant messages.
const RoleSystem = "system"
RoleSystem is the role for system messages (instructions to the model).
const RoleUser = "user"
RoleUser is the role for user messages.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Assembler ¶
type Assembler struct {
// contains filtered or unexported fields
}
Assembler builds prompts from components based on profiles and context.
func NewAssembler ¶
NewAssembler creates a new assembler with the given registry. Assemble will fail if any referenced component is not found.
func (*Assembler) Assemble ¶
Assemble builds a complete prompt for the given profile and context. Returns an error if any referenced component is not found. If the profile has an InputSchema, defaults are applied and required inputs are validated automatically before assembly.
Example (Conditional) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("base", `
name: base
instructions: Base instructions.
`)
store.AddComponent("premium", `
name: premium
condition: is_premium
instructions: Premium features enabled.
`)
store.AddProfile("app", `
name: app
components:
- base
- premium
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Without premium flag
ctx := promptloom.NewContext()
prompt, _ := assembler.Assemble("app", ctx)
fmt.Println("Without premium:")
fmt.Println(prompt)
// With premium flag
ctx = promptloom.NewContext().SetFlag("is_premium", true)
prompt, _ = assembler.Assemble("app", ctx)
fmt.Println("\nWith premium:")
fmt.Println(prompt)
}
Output: Without premium: Base instructions. With premium: Base instructions. Premium features enabled.
Example (EnhancedConditions) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("basic", `
name: basic
instructions: Basic instructions.
`)
store.AddComponent("premium", `
name: premium
condition: tier == 'premium'
instructions: Premium features enabled.
`)
store.AddComponent("high_volume", `
name: high_volume
condition: request_count > 100
instructions: High volume mode active.
`)
store.AddProfile("app", `
name: app
components:
- basic
- premium
- high_volume
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Free tier with low volume
ctx := promptloom.NewContext().
SetValue("tier", "free").
SetNumber("request_count", 50)
prompt, _ := assembler.Assemble("app", ctx)
fmt.Println("Free tier:")
fmt.Println(prompt)
// Premium tier with high volume
ctx = promptloom.NewContext().
SetValue("tier", "premium").
SetNumber("request_count", 150)
prompt, _ = assembler.Assemble("app", ctx)
fmt.Println("\nPremium tier:")
fmt.Println(prompt)
}
Output: Free tier: Basic instructions. Premium tier: Basic instructions. Premium features enabled. High volume mode active.
Example (MissingComponent) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("greeting", `
name: greeting
instructions: Hello!
`)
store.AddProfile("valid", `
name: valid
components:
- greeting
`)
store.AddProfile("invalid", `
name: invalid
components:
- greeting
- missing_component
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Valid profile succeeds
prompt, err := assembler.Assemble("valid", promptloom.NewContext())
if err == nil {
fmt.Println("Valid profile:", prompt)
}
// Invalid profile fails (missing component)
_, err = assembler.Assemble("invalid", promptloom.NewContext())
if err != nil {
fmt.Println("Invalid profile error:", err)
}
}
Output: Valid profile: Hello! Invalid profile error: component not found: missing_component
func (*Assembler) AssembleMessages ¶
AssembleMessages builds a prompt as a slice of messages for the given profile and context. Components are grouped by role, with adjacent same-role components merged into single messages. Returns an error if any referenced component is not found.
func (*Assembler) AssembleMessagesWithTrace ¶
func (a *Assembler) AssembleMessagesWithTrace(profileName string, ctx *Context) ([]Message, *AssemblyTrace, error)
AssembleMessagesWithTrace builds a prompt as messages and returns detailed trace information. Returns an error if any referenced component is not found.
func (*Assembler) AssembleWithTrace ¶
func (a *Assembler) AssembleWithTrace(profileName string, ctx *Context) (string, *AssemblyTrace, error)
AssembleWithTrace builds a prompt and returns detailed trace information about the assembly process. Useful for debugging complex profiles. Returns an error if any referenced component is not found. If the profile has an InputSchema, defaults are applied and required inputs are validated automatically before assembly.
Example (ConditionalComponents) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("always", `
name: always
instructions: Always included.
`)
store.AddComponent("conditional", `
name: conditional
condition: enabled
instructions: Conditionally included.
`)
store.AddProfile("test", `
name: test
components:
- always
- conditional
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
ctx := promptloom.NewContext().SetFlag("enabled", false)
_, trace, _ := assembler.AssembleWithTrace("test", ctx)
fmt.Println("Included:", trace.IncludedComponents)
fmt.Println("Skipped:")
for _, s := range trace.SkippedComponents {
fmt.Printf(" - %s: %s\n", s.Name, s.Reason)
}
}
Output: Included: [always] Skipped: - conditional: condition "enabled" is false
Example (WithMetadata) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("greeting", `
name: greeting
instructions: Hello! I'm your AI assistant.
`)
store.AddProfile("assistant", `
name: assistant
components:
- greeting
metadata:
model_id: "gpt-4"
provider: "openai"
max_tokens: 4096
overrides:
preferred_keywords:
- helpful
- concise
suffix: "Always be helpful."
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
prompt, trace, _ := assembler.AssembleWithTrace("assistant", promptloom.NewContext())
profile := trace.ResolvedProfile
fmt.Println("Prompt:", prompt)
fmt.Println("Model:", profile.GetDataString("model_id"))
fmt.Println("Keywords:", profile.Overrides.PreferredKeywords)
}
Output: Prompt: Hello! I'm your AI assistant. Always be helpful. Model: gpt-4 Keywords: [helpful concise]
func (*Assembler) ClearCache ¶
func (a *Assembler) ClearCache()
ClearCache clears the resolved profile cache. Call this if you modify profiles in the registry after assembly.
func (*Assembler) GetAvoidKeywords ¶
GetAvoidKeywords returns keywords to avoid for a profile.
func (*Assembler) GetInputSchema ¶
func (a *Assembler) GetInputSchema(profileName string) *InputSchema
GetInputSchema returns the effective InputSchema for a profile (with inheritance resolved). Returns nil if the profile doesn't exist or has no InputSchema. This method is useful for introspection and documentation generation.
func (*Assembler) GetOverrides ¶
func (a *Assembler) GetOverrides(profileName string) *ProfileOverrides
GetOverrides returns the effective overrides for a profile (with inheritance resolved). The returned pointer references the cached profile's Overrides field; callers should not modify it.
func (*Assembler) GetPreferredKeywords ¶
GetPreferredKeywords returns preferred keywords for a profile.
type AssemblyTrace ¶
type AssemblyTrace struct {
// ProfileName is the name of the profile being assembled
ProfileName string
// ResolvedProfile is the effective profile after inheritance resolution
ResolvedProfile *Profile
// IncludedComponents lists components that were included in the output
IncludedComponents []string
// SkippedComponents lists components that were skipped with reasons
SkippedComponents []SkippedComponent
// IncludedSections lists sections that were included
IncludedSections []IncludedSection
// SkippedSections lists sections that were skipped with reasons
SkippedSections []SkippedSection
// ConditionResults maps condition expressions to their evaluation results
ConditionResults map[string]bool
// ConditionErrors records errors that occurred during condition evaluation
ConditionErrors []ConditionError
}
AssemblyTrace captures details about the prompt assembly process.
type Component ¶
type Component struct {
// Name is the unique identifier for this component
Name string `yaml:"name"`
// Description explains what this component does
Description string `yaml:"description"`
// Condition is an optional expression that determines if this component is included
// Supports: simple flags (has_agent), negation (!has_agent), AND (a && b), OR (a || b)
Condition string `yaml:"condition,omitempty"`
// DependsOn specifies other components that must be included for this component to be included.
// If any dependency was not included (due to condition evaluation or not being in the profile),
// this component is automatically skipped. Dependencies must appear earlier in the profile's
// component list. Accepts either a single string or a list of strings in YAML:
// depends_on: foo # single dependency
// depends_on: [foo, bar] # multiple dependencies
DependsOn StringOrSlice `yaml:"depends_on,omitempty"`
// Role specifies the message role for this component when using AssembleMessages.
// Valid values: "system", "user", "assistant". Defaults to "system" if not specified.
// Adjacent components with the same role are merged into a single message.
// All ordered sections within a component inherit this role and the template setting.
Role string `yaml:"role,omitempty"`
// Instructions is the main prompt text for this component
Instructions string `yaml:"instructions"`
// Template enables text/template processing for Instructions and OrderedSections.
// When true, the text is parsed as a Go template with access to Context data.
// Available template variables:
// {{.Flags.flag_name}} - boolean flags
// {{.Values.value_name}} - string values
// {{.Numbers.number_name}} - integer values
// {{.Data.data_name}} - arbitrary data
// {{.Component.Name}}, {{.Component.Data}} - component metadata
Template bool `yaml:"template,omitempty"`
// OrderedSections contains sections with explicit ordering, conditions, and priorities.
// Sections are processed in priority order (highest first), and each section can have
// its own condition expression. This is the recommended format for new components.
//
// Example YAML:
// ordered_sections:
// - name: premium
// condition: tier == 'premium'
// priority: 100
// content: Premium features enabled.
// - name: basic
// priority: 50
// content: Basic instructions.
OrderedSections []Section `yaml:"ordered_sections,omitempty"`
// Data holds arbitrary structured data for use in templates.
// May be nil if no data is defined. Use GetDataString, GetDataStrings, etc.
// for type-safe access with nil handling.
Data map[string]any `yaml:"data,omitempty"`
}
Component represents a reusable prompt fragment. Components can have conditions that determine when they're included.
Example (OrderedSections) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("features", `
name: features
instructions: Available features based on your plan.
ordered_sections:
- name: critical
priority: 100
content: Core features available to all users.
- name: premium
priority: 50
condition: is_premium
content: Premium features unlocked!
- name: trial
priority: 25
condition: is_trial
content: Trial features (limited time).
`)
store.AddProfile("app", `
name: app
components:
- features
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Premium user sees both critical and premium sections
ctx := promptloom.NewContext().SetFlag("is_premium", true)
prompt, _ := assembler.Assemble("app", ctx)
fmt.Println(prompt)
}
Output: Available features based on your plan. Core features available to all users. Premium features unlocked!
Example (OrderedSections_priority) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("instructions", `
name: instructions
instructions: System instructions.
ordered_sections:
- name: safety
priority: 100
content: "[SAFETY] Always prioritize user safety."
- name: helpful
priority: 75
content: "[HELPFUL] Be helpful and informative."
- name: concise
priority: 50
content: "[CONCISE] Keep responses brief."
`)
store.AddProfile("assistant", `
name: assistant
components:
- instructions
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Sections are ordered by priority (highest first)
prompt, _ := assembler.Assemble("assistant", promptloom.NewContext())
fmt.Println(prompt)
}
Output: System instructions. [SAFETY] Always prioritize user safety. [HELPFUL] Be helpful and informative. [CONCISE] Keep responses brief.
Example (Template) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("greeting", `
name: greeting
template: true
instructions: |
Hello, {{.Values.user_name}}!
You have {{.Numbers.message_count}} unread messages.
`)
store.AddProfile("test", `
name: test
components:
- greeting
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
ctx := promptloom.NewContext().
SetValue("user_name", "Bob").
SetNumber("message_count", 5)
prompt, _ := assembler.Assemble("test", ctx)
fmt.Println(prompt)
}
Output: Hello, Bob! You have 5 unread messages.
func (*Component) GetDataBool ¶
GetDataBool returns a bool value from component data, or false if not found.
func (*Component) GetDataInt ¶
GetDataInt returns an int value from component data, or 0 if not found.
func (*Component) GetDataString ¶
GetDataString returns a string value from component data, or empty string if not found.
func (*Component) GetDataStrings ¶
GetDataStrings returns a string slice from component data, or nil if not found.
func (*Component) GetDependencies ¶
GetDependencies returns the component's dependencies as a slice. Returns nil if no dependencies are defined.
type ComponentData ¶
type ComponentData struct {
// Name is the component's name
Name string
// Description is the component's description
Description string
// Data is the component's custom data
Data map[string]any
}
ComponentData contains component metadata available in templates.
type ConditionError ¶
type ConditionError struct {
// Condition is the original condition expression
Condition string
// Phase indicates where the error occurred: "compile" or "run"
Phase string
// Err is the underlying error
Err error
}
ConditionError records an error that occurred during condition evaluation.
func (*ConditionError) Error ¶
func (e *ConditionError) Error() string
Error implements the error interface.
func (*ConditionError) Unwrap ¶
func (e *ConditionError) Unwrap() error
Unwrap returns the underlying error.
type Context ¶
type Context struct {
// Flags are boolean conditions (e.g., "has_agent", "is_premium")
Flags map[string]bool
// Values are string values (e.g., "model_name", "user_locale")
Values map[string]string
// Numbers are numeric values (e.g., "scene_count", "max_tokens")
Numbers map[string]int
// Data holds arbitrary structured data
Data map[string]any
// Provider identifies the LLM provider for automatic profile resolution.
// When set, assembly of profile "X" will use "X.provider" if it exists.
Provider string
}
Context holds runtime values used for condition evaluation and template rendering.
Example ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
)
func main() {
ctx := promptloom.NewContext().
SetFlag("premium", true).
SetFlag("verbose", false).
SetValue("user_name", "Alice").
SetNumber("max_tokens", 4096).
SetData("preferences", map[string]string{"theme": "dark"})
fmt.Println("Premium:", ctx.GetFlag("premium"))
fmt.Println("User:", ctx.GetValue("user_name"))
fmt.Println("Tokens:", ctx.GetNumber("max_tokens"))
}
Output: Premium: true User: Alice Tokens: 4096
Example (Batch_setters) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
)
func main() {
ctx := promptloom.NewContext().
SetFlags(map[string]bool{
"premium": true,
"verbose": false,
}).
SetValues(map[string]string{
"tier": "gold",
"user_name": "Alice",
}).
SetNumbers(map[string]int{
"max_tokens": 4096,
"timeout": 30,
})
fmt.Println("Premium:", ctx.GetFlag("premium"))
fmt.Println("Tier:", ctx.GetValue("tier"))
fmt.Println("Max Tokens:", ctx.GetNumber("max_tokens"))
}
Output: Premium: true Tier: gold Max Tokens: 4096
func (*Context) GetNestedValue ¶
GetNestedValue retrieves a value from nested Data using dot notation. For example, "user.profile.name" accesses Data["user"]["profile"]["name"]. Returns the value and true if found, nil and false otherwise. The maximum nesting depth is bounded by the path string length.
func (*Context) GetProvider ¶
GetProvider returns the LLM provider.
func (*Context) HasData ¶
HasData returns true if the data key exists in the context (regardless of its value).
func (*Context) HasFlag ¶
HasFlag returns true if the flag exists in the context (regardless of its value).
func (*Context) SetNumbers ¶
SetNumbers sets multiple numeric values at once.
func (*Context) SetProvider ¶
SetProvider sets the LLM provider for automatic profile resolution. When set, assembly of profile "X" will use "X.provider" if it exists.
type IncludedSection ¶
type IncludedSection struct {
// ComponentName is the parent component
ComponentName string
// SectionName is the section that was included
SectionName string
}
IncludedSection records which sections were included.
type InputField ¶
type InputField struct {
// Name is the input field name (used in Context)
Name string `yaml:"name"`
// Type is the expected type: "flag", "string", "number", or "data"
Type string `yaml:"type,omitempty"`
// Required indicates if this input must be provided
Required bool `yaml:"required,omitempty"`
// Default is the default value if not provided (for documentation)
Default any `yaml:"default,omitempty"`
// Description explains what this input is for
Description string `yaml:"description,omitempty"`
}
InputField describes a single expected input.
type InputSchema ¶
type InputSchema struct {
// Inputs lists the expected input fields
Inputs []InputField `yaml:"inputs,omitempty"`
}
InputSchema declares the expected inputs for a profile or component. This serves as documentation and enables validation in strict mode.
Example YAML:
input_schema:
inputs:
- name: user_name
type: string
required: true
description: The user's display name
- name: is_premium
type: flag
default: false
- name: max_tokens
type: number
required: true
Example ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("greeting", `
name: greeting
template: true
instructions: |
Hello, {{.Values.user_name}}!
Your tier: {{.Values.tier}}
`)
store.AddProfile("app", `
name: app
components:
- greeting
input_schema:
inputs:
- name: user_name
type: string
required: true
description: The user's display name
- name: tier
type: string
required: true
description: User subscription tier
- name: is_premium
type: flag
required: false
description: Whether user has premium features
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Get input schema for documentation
profile := registry.GetProfile("app")
fmt.Println("Required inputs:")
for _, name := range profile.InputSchema.RequiredInputNames() {
input := profile.InputSchema.GetInput(name)
fmt.Printf(" - %s: %s\n", name, input.Description)
}
// Validation happens automatically during Assemble
ctx := promptloom.NewContext().
SetValue("user_name", "Alice").
SetValue("tier", "gold")
prompt, err := assembler.Assemble("app", ctx)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("\nGenerated prompt:")
fmt.Println(prompt)
}
Output: Required inputs: - user_name: The user's display name - tier: User subscription tier Generated prompt: Hello, Alice! Your tier: gold
func (*InputSchema) ApplyDefaults ¶
func (s *InputSchema) ApplyDefaults(ctx *Context)
ApplyDefaults fills in missing optional inputs with their default values. This should be called before Validate() to ensure defaults are applied.
func (*InputSchema) GetInput ¶
func (s *InputSchema) GetInput(name string) *InputField
GetInput returns the InputField for the given name, or nil if not found.
func (*InputSchema) RequiredInputNames ¶
func (s *InputSchema) RequiredInputNames() []string
RequiredInputNames returns the names of all required inputs.
func (*InputSchema) Validate ¶
func (s *InputSchema) Validate(ctx *Context) error
Validate checks if the given context satisfies the input schema. Returns nil if valid, or an error describing what's missing or type mismatches.
type LoadOption ¶
type LoadOption func(*loadOptions)
LoadOption configures the behavior of LoadFromFS.
func WithComponentsDir ¶
func WithComponentsDir(dir string) LoadOption
WithComponentsDir sets the directory name for components (default: "components").
func WithProfilesDir ¶
func WithProfilesDir(dir string) LoadOption
WithProfilesDir sets the directory name for profiles (default: "profiles").
type Message ¶
type Message struct {
// Role is the message role: "system", "user", or "assistant".
// This maps directly to the role field expected by most LLM APIs.
Role string
// Content is the text content of the message.
Content string
}
Message represents a single message in a conversation. Used by AssembleMessages to return structured prompts instead of a single string.
type Profile ¶
type Profile struct {
// Name is the unique identifier for this profile
Name string `yaml:"name"`
// Description explains what this profile is for
Description string `yaml:"description"`
// Version is the semantic version of this profile (e.g., "1.0.0", "2.1.0-beta").
// Inherited when a profile inherits from another.
Version string `yaml:"version,omitempty"`
// Tags are labels for organizing profiles (e.g., ["production", "vision"]).
// Tags are merged during inheritance. Use slices.Contains for membership checks.
Tags []string `yaml:"tags,omitempty"`
// Inherits specifies a parent profile to inherit from
Inherits string `yaml:"inherits,omitempty"`
// Provider identifies the LLM provider this profile is optimized for (e.g., "anthropic", "openai").
// Used for automatic profile resolution: when context has a provider set,
// assembly of "X" will use "X.provider" if it exists.
Provider string `yaml:"provider,omitempty"`
// Components lists the component names to include (in order)
Components []string `yaml:"components"`
// Overrides contains profile-specific overrides for component settings
Overrides ProfileOverrides `yaml:"overrides"`
// ModelInstructions contains additional model-specific instructions
ModelInstructions string `yaml:"model_instructions,omitempty"`
// Metadata holds arbitrary profile-level data (e.g., model info, fidelity settings).
// May be nil if no metadata is defined. Use GetDataString, GetDataStrings, etc.
// for type-safe access with nil handling.
Metadata map[string]any `yaml:"metadata,omitempty"`
// InputSchema declares the expected inputs for this profile.
// Used for documentation and validation in strict mode.
InputSchema *InputSchema `yaml:"input_schema,omitempty"`
}
Profile represents a model-specific prompt configuration. Profiles can inherit from other profiles to share common settings.
Example (Inheritance) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
store.AddComponent("base", `
name: base
instructions: Base component.
`)
store.AddProfile("parent", `
name: parent
components:
- base
overrides:
suffix: Parent suffix.
`)
store.AddProfile("child", `
name: child
inherits: parent
overrides:
suffix: Child suffix.
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath())
assembler := promptloom.NewAssembler(registry)
// Child inherits components from parent but overrides suffix
prompt, _ := assembler.Assemble("child", promptloom.NewContext())
fmt.Println(prompt)
}
Output: Base component. Child suffix.
func (*Profile) GetDataBool ¶
GetDataBool returns a bool value from metadata, or false if not found.
func (*Profile) GetDataInt ¶
GetDataInt returns an int value from metadata, or 0 if not found.
func (*Profile) GetDataString ¶
GetDataString returns a string value from metadata, or empty string if not found.
Example ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
)
func main() {
profile := &promptloom.Profile{
Name: "example",
Metadata: map[string]any{
"model_id": "gpt-4",
"provider": "openai",
"max_tokens": 4096,
"supports_vision": true,
},
}
fmt.Println("Model:", profile.GetDataString("model_id"))
fmt.Println("Provider:", profile.GetDataString("provider"))
fmt.Println("Max Tokens:", profile.GetDataInt("max_tokens"))
fmt.Println("Vision:", profile.GetDataBool("supports_vision"))
}
Output: Model: gpt-4 Provider: openai Max Tokens: 4096 Vision: true
func (*Profile) GetDataStrings ¶
GetDataStrings returns a string slice from metadata, or nil if not found.
type ProfileOverrides ¶
type ProfileOverrides struct {
// PreferredKeywords are keywords the model responds well to
PreferredKeywords []string `yaml:"preferred_keywords,omitempty"`
// AvoidKeywords are keywords that cause issues with this model
AvoidKeywords []string `yaml:"avoid_keywords,omitempty"`
// Suffix is additional text appended to the prompt
Suffix string `yaml:"suffix,omitempty"`
// Custom holds arbitrary override data
Custom map[string]any `yaml:"custom,omitempty"`
}
ProfileOverrides contains settings that override component defaults.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds all loaded prompt components and profiles.
func (*Registry) GetComponent ¶
GetComponent returns a component by name.
func (*Registry) GetProfile ¶
GetProfile returns a profile by name.
func (*Registry) GetProfilesByTag ¶
GetProfilesByTag returns all profiles that have the specified tag. The returned slice contains pointers to the registry's internal profiles. Callers must not modify the returned profiles; doing so affects the registry's state. For safe modification, copy any data you need before mutating.
func (*Registry) GetProviderVariants ¶
GetProviderVariants returns all provider names that have variants for the base profile. For example, if "assistant.anthropic" and "assistant.openai" exist, calling GetProviderVariants("assistant") returns ["anthropic", "openai"].
func (*Registry) HasProviderVariant ¶
HasProviderVariant checks if a specific provider variant exists for the base profile. Returns true only if the variant exists AND properly inherits from the base.
func (*Registry) ListComponents ¶
ListComponents returns all component names.
func (*Registry) ListProfiles ¶
ListProfiles returns all profile names.
func (*Registry) LoadFromFS ¶
LoadFromFS loads components and profiles from a filesystem. By default, it expects a "components" directory for components and a "profiles" directory for profiles. Use WithComponentsDir and WithProfilesDir to customize.
Example (CustomDirs) ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
"github.com/lirancohen/promptloom/storage"
)
func main() {
store := storage.NewMemoryStorage()
// Add component to custom "snippets" directory
store.AddFile("snippets/greeting.yaml", `
name: greeting
instructions: Hello from custom directory!
`)
// Add profile to custom "configs" directory
store.AddFile("configs/main.yaml", `
name: main
components:
- greeting
`)
registry := promptloom.NewRegistry()
_ = registry.LoadFromFS(store.FS(), store.RootPath(),
promptloom.WithComponentsDir("snippets"),
promptloom.WithProfilesDir("configs"),
)
assembler := promptloom.NewAssembler(registry)
prompt, _ := assembler.Assemble("main", promptloom.NewContext())
fmt.Println(prompt)
}
Output: Hello from custom directory!
func (*Registry) RegisterComponent ¶
RegisterComponent adds a component to the registry. Panics if the component is nil or has an empty name.
func (*Registry) RegisterProfile ¶
RegisterProfile adds a profile to the registry. Panics if the profile is nil or has an empty name.
func (*Registry) Validate ¶
Validate checks the registry for configuration errors. By default, it only checks for circular inheritance. Use ValidateStrict() or pass options to enable additional checks.
Example ¶
package main
import (
"fmt"
"github.com/lirancohen/promptloom"
)
func main() {
registry := promptloom.NewRegistry()
// Register a profile with circular inheritance
registry.RegisterProfile(&promptloom.Profile{Name: "a", Inherits: "b"})
registry.RegisterProfile(&promptloom.Profile{Name: "b", Inherits: "a"})
err := registry.Validate()
if err != nil {
fmt.Println("Validation error detected")
}
}
Output: Validation error detected
func (*Registry) ValidateConditions ¶
ValidateConditions validates all condition expressions in the registry against the given context. This allows upfront detection of malformed conditions before assembly. Returns a slice of errors for any invalid conditions found. Returns nil if all conditions are valid.
func (*Registry) ValidateStrict ¶
ValidateStrict checks the registry for all configuration errors including unknown component and profile references.
func (*Registry) ValidateWithOptions ¶
func (r *Registry) ValidateWithOptions(opts ValidateOptions) error
ValidateWithOptions checks the registry with the specified options.
type Section ¶
type Section struct {
// Name is the unique identifier for this section within the component
Name string `yaml:"name"`
// Content is the text content of this section
Content string `yaml:"content"`
// Condition is an optional expression that determines if this section is included.
// Supports the same syntax as component conditions:
// - Simple flags: "has_agent"
// - Negation: "!has_agent"
// - AND: "has_agent && is_premium"
// - OR: "has_agent || is_demo"
// - String comparisons: "tier == 'premium'"
// - Numeric comparisons: "count > 10"
Condition string `yaml:"condition,omitempty"`
// Priority determines the order of inclusion (higher = included earlier).
// Sections are sorted by priority in descending order, so higher priority
// sections appear before lower priority sections in the assembled prompt.
// Default is 0. Sections with equal priority maintain their definition order.
Priority int `yaml:"priority,omitempty"`
}
Section represents an ordered, conditional section within a component. Sections allow for fine-grained control over prompt content with support for conditions and priority-based ordering.
type SkippedComponent ¶
type SkippedComponent struct {
// Name is the component name
Name string
// Reason explains why it was skipped
Reason string
}
SkippedComponent records why a component was not included.
type SkippedSection ¶
type SkippedSection struct {
// ComponentName is the parent component
ComponentName string
// SectionName is the section that was skipped
SectionName string
// Reason explains why it was skipped
Reason string
}
SkippedSection records which sections were skipped and why.
type StringOrSlice ¶
type StringOrSlice []string
StringOrSlice is a type that can unmarshal from either a YAML string or sequence. This provides flexible YAML syntax: depends_on: foo OR depends_on: [foo, bar]
func (*StringOrSlice) UnmarshalYAML ¶
func (s *StringOrSlice) UnmarshalYAML(value *yaml.Node) error
UnmarshalYAML implements yaml.Unmarshaler for StringOrSlice.
type TemplateData ¶
type TemplateData struct {
// Flags contains boolean flags from the context
Flags map[string]bool
// Values contains string values from the context
Values map[string]string
// Numbers contains integer values from the context
Numbers map[string]int
// Data contains arbitrary data from the context
Data map[string]any
// Component contains metadata about the current component
Component ComponentData
}
TemplateData is the data structure passed to component templates.
type ValidateOptions ¶
type ValidateOptions struct {
// Strict enables strict mode which reports errors for:
// - Profiles referencing unknown components
// - Profiles inheriting from unknown parents
// In lenient mode (default), these are silently ignored at assembly time.
Strict bool
}
ValidateOptions configures validation behavior.
type ValidationError ¶
type ValidationError struct {
Errors []string
}
ValidationError represents one or more validation issues found in the registry.
func (*ValidationError) Add ¶
func (e *ValidationError) Add(format string, args ...any)
Add appends an error message to the validation error.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
func (*ValidationError) HasErrors ¶
func (e *ValidationError) HasErrors() bool
HasErrors returns true if any validation errors were recorded.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package format provides utilities for converting PromptLoom messages to provider-specific formats.
|
Package format provides utilities for converting PromptLoom messages to provider-specific formats. |
|
anthropic
Package anthropic provides formatting for Anthropic Messages API.
|
Package anthropic provides formatting for Anthropic Messages API. |
|
openai
Package openai provides formatting for OpenAI Chat Completions API.
|
Package openai provides formatting for OpenAI Chat Completions API. |
|
Package storage provides storage interfaces for prompt components and profiles.
|
Package storage provides storage interfaces for prompt components and profiles. |