Packages
nous
0.13.3
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/prompt_template.ex
defmodule Nous.PromptTemplate do
@moduledoc """
EEx-based prompt templates for building messages.
Supports variable substitution, role assignment, and composition
of messages for agent interactions.
## Simple Usage
template = PromptTemplate.from_template(
"You are a <%= @role %> assistant that speaks <%= @language %>.",
role: :system
)
message = PromptTemplate.to_message(template, %{role: "helpful", language: "Spanish"})
# => %Message{role: :system, content: "You are a helpful assistant that speaks Spanish."}
## Building Message Lists
messages = PromptTemplate.to_messages([
PromptTemplate.from_template("You are <%= @persona %>", role: :system),
PromptTemplate.from_template("Tell me about <%= @topic %>", role: :user)
], %{persona: "a historian", topic: "ancient Rome"})
# Use with Agent.run
Agent.run(agent, messages: messages)
## Default Values
template = PromptTemplate.from_template(
"Search for <%= @query %> with limit <%= @limit %>",
inputs: %{limit: 10}
)
# Only need to provide query, limit has a default
formatted = PromptTemplate.format(template, %{query: "elixir"})
# => "Search for elixir with limit 10"
## Conditional Content
Since templates use EEx, you can use conditionals:
template = PromptTemplate.from_template(\"""
You are a helpful assistant.
<%= if @include_tools do %>
You have access to these tools: <%= @tools %>
<% end %>
\""")
## String Templates
For simple string templates without Message conversion:
text = PromptTemplate.format_string(
"Hello, <%= @name %>!",
%{name: "World"}
)
# => "Hello, World!"
"""
alias Nous.Message
@type t :: %__MODULE__{
text: String.t(),
role: :system | :user | :assistant,
inputs: map()
}
@enforce_keys [:text]
defstruct [:text, role: :user, inputs: %{}]
@doc """
Create a template from a string with `<%= @var %>` placeholders.
## Options
- `:role` - Message role (`:system`, `:user`, `:assistant`). Default: `:user`
- `:inputs` - Default variable values. Default: `%{}`
## Example
template = PromptTemplate.from_template(
"You are a <%= @role %> assistant.",
role: :system,
inputs: %{role: "helpful"}
)
"""
@spec from_template(String.t(), keyword()) :: t()
def from_template(text, opts \\ []) when is_binary(text) do
%__MODULE__{
text: text,
role: Keyword.get(opts, :role, :user),
inputs: Keyword.get(opts, :inputs, %{})
}
end
@doc """
Create a system message template.
Shorthand for `from_template(text, role: :system)`.
## Example
template = PromptTemplate.system("You are a <%= @persona %>")
"""
@spec system(String.t(), keyword()) :: t()
def system(text, opts \\ []) when is_binary(text) do
from_template(text, Keyword.put(opts, :role, :system))
end
@doc """
Create a user message template.
Shorthand for `from_template(text, role: :user)`.
## Example
template = PromptTemplate.user("Tell me about <%= @topic %>")
"""
@spec user(String.t(), keyword()) :: t()
def user(text, opts \\ []) when is_binary(text) do
from_template(text, Keyword.put(opts, :role, :user))
end
@doc """
Create an assistant message template.
Shorthand for `from_template(text, role: :assistant)`.
## Example
template = PromptTemplate.assistant("I understand you want <%= @action %>")
"""
@spec assistant(String.t(), keyword()) :: t()
def assistant(text, opts \\ []) when is_binary(text) do
from_template(text, Keyword.put(opts, :role, :assistant))
end
@doc """
Format template with variable substitution.
Merges provided bindings with default inputs (bindings take precedence).
## Example
template = from_template("Hello, <%= @name %>!", inputs: %{name: "World"})
format(template, %{}) # => "Hello, World!"
format(template, %{name: "Elixir"}) # => "Hello, Elixir!"
"""
@spec format(t(), map()) :: String.t()
def format(%__MODULE__{text: text, inputs: default_inputs}, bindings \\ %{}) do
merged = Map.merge(default_inputs, bindings)
do_format(text, merged)
end
@doc """
Format a string template directly without creating a PromptTemplate struct.
## Example
PromptTemplate.format_string("Hello, <%= @name %>!", %{name: "World"})
# => "Hello, World!"
"""
@spec format_string(String.t(), map()) :: String.t()
def format_string(text, bindings) when is_binary(text) and is_map(bindings) do
do_format(text, bindings)
end
@doc """
Convert template to Message with variable substitution.
## Example
template = from_template("You are a <%= @role %> assistant", role: :system)
message = to_message(template, %{role: "helpful"})
# => %Message{role: :system, content: "You are a helpful assistant"}
"""
@spec to_message(t(), map()) :: Message.t()
def to_message(%__MODULE__{role: role} = template, bindings \\ %{}) do
content = format(template, bindings)
case role do
:system -> Message.system(content)
:user -> Message.user(content)
:assistant -> Message.assistant(content)
end
end
@doc """
Convert list of templates and/or messages to messages.
Items can be:
- `%PromptTemplate{}` - Will be formatted with bindings
- `%Message{}` - Passed through unchanged
## Example
messages = to_messages([
PromptTemplate.system("You are <%= @persona %>"),
Message.user("Hello"),
PromptTemplate.user("Tell me about <%= @topic %>")
], %{persona: "helpful", topic: "Elixir"})
"""
@spec to_messages([t() | Message.t()], map()) :: [Message.t()]
def to_messages(items, bindings \\ %{}) when is_list(items) do
Enum.map(items, fn
%__MODULE__{} = template -> to_message(template, bindings)
%Message{} = message -> message
end)
end
@doc """
Create messages from a list of role/content tuples with shared bindings.
Convenient for building message lists inline.
## Example
messages = PromptTemplate.build_messages([
{:system, "You are a <%= @role %> assistant"},
{:user, "Hello, my name is <%= @name %>"}
], %{role: "helpful", name: "Alice"})
"""
@spec build_messages([{atom(), String.t()}], map()) :: [Message.t()]
def build_messages(items, bindings \\ %{}) when is_list(items) do
Enum.map(items, fn {role, text} ->
template = from_template(text, role: role)
to_message(template, bindings)
end)
end
@doc """
Extract all variable names from a template.
Useful for validation or documentation.
## Example
template = from_template("Hello <%= @name %>, you are <%= @age %> years old")
variables(template)
# => [:name, :age]
"""
@spec variables(t()) :: [atom()]
def variables(%__MODULE__{text: text}) do
extract_variables(text)
end
@doc """
Extract variable names from a template string.
## Example
PromptTemplate.extract_variables("Hello <%= @name %>!")
# => [:name]
"""
@spec extract_variables(String.t()) :: [atom()]
def extract_variables(text) when is_binary(text) do
Regex.scan(~r/@(\w+)/, text)
|> Enum.map(fn [_, var] -> String.to_atom(var) end)
|> Enum.uniq()
end
@doc """
Check if all required variables are present in bindings.
Returns `{:ok, bindings}` if all variables are present,
or `{:error, missing_vars}` if some are missing.
## Example
template = from_template("Hello <%= @name %>, age <%= @age %>")
validate_bindings(template, %{name: "Alice", age: 30})
# => {:ok, %{name: "Alice", age: 30}}
validate_bindings(template, %{name: "Alice"})
# => {:error, [:age]}
"""
@spec validate_bindings(t(), map()) :: {:ok, map()} | {:error, [atom()]}
def validate_bindings(%__MODULE__{} = template, bindings) do
required = variables(template)
available = Map.merge(template.inputs, bindings)
provided = MapSet.new(Map.keys(available))
required_set = MapSet.new(required)
missing = MapSet.difference(required_set, provided) |> MapSet.to_list()
if Enum.empty?(missing) do
{:ok, available}
else
{:error, missing}
end
end
@doc """
Compose multiple templates into a single template.
Joins templates with the specified separator.
## Example
intro = system("You are a helpful assistant.")
rules = system("Follow these rules: <%= @rules %>")
combined = compose([intro, rules], "\\n\\n")
# Creates a single :system template with both texts joined
"""
@spec compose([t()], String.t()) :: t()
def compose(templates, separator \\ "\n") when is_list(templates) do
# Use the role of the first template
role =
case templates do
[first | _] -> first.role
[] -> :user
end
# Merge all inputs
merged_inputs =
Enum.reduce(templates, %{}, fn template, acc ->
Map.merge(acc, template.inputs)
end)
# Join all texts
combined_text =
templates
|> Enum.map(& &1.text)
|> Enum.join(separator)
%__MODULE__{
text: combined_text,
role: role,
inputs: merged_inputs
}
end
# Private functions
defp do_format(text, bindings) do
assigns = Map.to_list(bindings)
EEx.eval_string(text, assigns: assigns)
end
end