PtcRunner.SubAgent.PromptExpander (PtcRunner v0.9.0)

Copy Markdown View Source

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

expand(template, context, opts \\ [])

@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}}"}

expand_annotated(template, context)

@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(template)

@spec extract_placeholder_names(String.t()) :: [String.t()]

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(template)

@spec extract_placeholders(String.t()) :: [%{path: [String.t()], type: :simple}]

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}]

extract_placeholders_with_sections(template)

@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_signature_params(signature)

@spec extract_signature_params(String.t()) :: [String.t()]

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")
[]