Template string expansion with placeholder validation.
Provides functions to:
- Extract placeholders from template strings
- Expand templates by replacing placeholders with values from a context map
Placeholder Syntax
Placeholders use {{variable}} syntax and support nested access with dot notation:
- Simple:
{{name}} - Nested:
{{user.name}}or{{items.count}}
Mustache Sections (Text Mode Only)
The expand/3 function supports Mustache sections for iterating over lists:
- List iteration:
{{#items}}{{name}} {{/items}} - Scalar lists with dot:
{{#tags}}{{.}} {{/tags}} - Inverted sections:
{{^items}}No items{{/items}}
Note: Sections are intended for text mode agents where data is embedded directly
in the prompt. For PTC-Lisp mode, use expand_annotated/2 which returns annotations
like ~{data/var} and does not support sections (the Data Inventory is flat).
Examples
iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{name: "Alice"})
{:ok, "Hello Alice"}
iex> PtcRunner.SubAgent.PromptExpander.expand("User {{user.name}}", %{user: %{name: "Bob"}})
{:ok, "User Bob"}
iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{})
{:error, {:missing_keys, ["name"]}}
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("Hello {{name}}, you have {{items.count}} items")
[%{path: ["name"], type: :simple}, %{path: ["items", "count"], type: :simple}]
Summary
Functions
Expand a template by replacing placeholders with values from the context.
Expand a template with annotations showing where substitutions occurred.
Extract placeholder names from a template string as a flat list.
Extract placeholders from a template string.
Extract all placeholders with full section information.
Extract parameter names from a SubAgent signature string.
Functions
@spec expand(String.t(), map(), keyword()) :: {:ok, String.t()} | {:error, {:missing_keys, [String.t()]}}
Expand a template by replacing placeholders with values from the context.
Returns {:ok, expanded_string} on success, or {:error, {:missing_keys, keys}}
if any placeholders cannot be resolved (when on_missing: :error).
The context map can use either atom or string keys. Values are converted to
strings using to_string/1.
Options
on_missing: Controls behavior when a placeholder key is missing from the context.:error(default) - Returns{:error, {:missing_keys, [...]}}if any keys are missing:keep- Leaves missing placeholders unchanged in the output (e.g.,"{{name}}")
Examples
iex> PtcRunner.SubAgent.PromptExpander.expand("Hello {{name}}", %{name: "Alice"})
{:ok, "Hello Alice"}
iex> PtcRunner.SubAgent.PromptExpander.expand("Count: {{count}}", %{count: 42})
{:ok, "Count: 42"}
iex> PtcRunner.SubAgent.PromptExpander.expand("{{a.b.c}}", %{a: %{b: %{c: "deep"}}})
{:ok, "deep"}
iex> PtcRunner.SubAgent.PromptExpander.expand("Hello", %{})
{:ok, "Hello"}
iex> PtcRunner.SubAgent.PromptExpander.expand("", %{})
{:ok, ""}
iex> PtcRunner.SubAgent.PromptExpander.expand("{{missing}}", %{})
{:error, {:missing_keys, ["missing"]}}
iex> PtcRunner.SubAgent.PromptExpander.expand("{{a}} and {{b}}", %{a: "1"})
{:error, {:missing_keys, ["b"]}}
iex> PtcRunner.SubAgent.PromptExpander.expand("{{missing}}", %{}, on_missing: :keep)
{:ok, "{{missing}}"}
iex> PtcRunner.SubAgent.PromptExpander.expand("{{a}} and {{b}}", %{a: "1"}, on_missing: :keep)
{:ok, "1 and {{b}}"}
@spec expand_annotated(String.t(), map()) :: {:ok, String.t()} | {:error, {:missing_keys, [String.t()]}}
Expand a template with annotations showing where substitutions occurred.
Returns an annotated string where substituted values are wrapped with ~{data/...}
syntax to make it clear which parts came from template variables. This is useful
for debugging to distinguish dynamic values from hardcoded text.
Examples
iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Hello {{name}}", %{name: "Alice"})
{:ok, "Hello ~{data/name}"}
iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Count: {{count}}", %{count: 42})
{:ok, "Count: ~{data/count}"}
iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("{{a.b}}", %{a: %{b: "deep"}})
{:ok, "~{data/a.b}"}
iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("Hello", %{})
{:ok, "Hello"}
iex> PtcRunner.SubAgent.PromptExpander.expand_annotated("{{missing}}", %{})
{:error, {:missing_keys, ["missing"]}}
Extract placeholder names from a template string as a flat list.
This is a convenience wrapper around extract_placeholders/1 that returns
only the placeholder names as flat strings (e.g., "name", "user.name").
Examples
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("Hello {{name}}")
["name"]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("{{user.name}} has {{count}} items")
["user.name", "count"]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("No placeholders here")
[]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholder_names("{{name}} and {{name}}")
["name"]
Extract placeholders from a template string.
Returns a list of unique placeholder structs, each containing:
path: List of strings representing the nested path (e.g., ["user", "name"])type: Always:simple(for backward compatibility, section names are flattened)
Examples
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("Hello {{name}}")
[%{path: ["name"], type: :simple}]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("{{user.name}} has {{count}} items")
[%{path: ["user", "name"], type: :simple}, %{path: ["count"], type: :simple}]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("No placeholders here")
[]
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders("{{name}} and {{name}}")
[%{path: ["name"], type: :simple}]
@spec extract_placeholders_with_sections(String.t()) :: [ PtcRunner.Mustache.variable_info() ]
Extract all placeholders with full section information.
Unlike extract_placeholders/1, this returns the complete variable structure
including section types and nested fields. Used for signature validation in Phase 3.
Examples
iex> PtcRunner.SubAgent.PromptExpander.extract_placeholders_with_sections("{{name}}")
[%{type: :simple, path: ["name"], fields: nil, loc: %{line: 1, col: 1}}]
iex> {:ok, [section]} = {:ok, PtcRunner.SubAgent.PromptExpander.extract_placeholders_with_sections("{{#items}}{{name}}{{/items}}")}
iex> section.type
:section
iex> section.path
["items"]
iex> [field] = section.fields
iex> field.path
["name"]
Extract parameter names from a SubAgent signature string.
Parses the signature and returns a list of parameter names. Returns an empty list if the signature cannot be parsed.
Examples
iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("(user :string) -> :string")
["user"]
iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("(name :string, age :int) -> :string")
["name", "age"]
iex> PtcRunner.SubAgent.PromptExpander.extract_signature_params("invalid signature")
[]