Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions dapr_agents/prompt/utils/fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#

from typing import Any, List
import re
from string import Formatter


def render_fstring_template(template: str, **kwargs: Any) -> str:
Expand All @@ -39,7 +39,17 @@ def extract_fstring_variables(template: str) -> List[str]:
Returns:
List[str]: A list of variable names found in the template.
"""
# Find all occurrences of {variable_name} in the template
# This will match {name}, {role}, etc. even when they're part of sentences
matches = re.findall(r"\{([^{}]+)\}", template)
return matches
# Use the stdlib string.Formatter, which shares str.format() semantics, so
# extraction matches how render_fstring_template renders the template. This
# correctly treats escaped braces ({{ and }}) as literal characters instead
# of variables, and drops any format spec (e.g. {value:>10} -> "value").
variables: List[str] = []
for _, field_name, _, _ in Formatter().parse(template):
if field_name is None:
continue
if field_name == "" or field_name.isdigit():
raise ValueError(
"Positional placeholders (e.g. '{}' or '{0}') are not supported; use named fields like '{name}'."
)
variables.append(field_name)
return variables
50 changes: 50 additions & 0 deletions tests/prompt/test_string_prompt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Copyright 2026 The Dapr Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Unit tests for StringPromptTemplate.format_prompt() method."""

import pytest
from dapr_agents.prompt.string import StringPromptTemplate


class TestStringPromptTemplateFormatPrompt:
"""Tests for StringPromptTemplate.format_prompt() method."""

def test_format_prompt_replaces_variable(self):
"""A simple placeholder is replaced with the provided value."""
template = StringPromptTemplate.from_template("Hello {name}")
assert template.format_prompt(name="Alice") == "Hello Alice"

def test_format_prompt_with_escaped_braces(self):
"""Escaped braces in a template do not trigger a missing-variable error.

A literal JSON example (using {{ }}) is a common prompting pattern; the
escaped content must not be treated as a required variable.
"""
template = StringPromptTemplate.from_template(
'Respond as JSON like {{"a": 1}}. Q: {question}'
)
assert template.input_variables == ["question"]
result = template.format_prompt(question="x")
assert result == 'Respond as JSON like {"a": 1}. Q: x'

def test_from_template_ignores_escaped_braces_in_variables(self):
"""Escaped braces are excluded from the detected input variables."""
template = StringPromptTemplate.from_template("{{literal}} and {real}")
assert template.input_variables == ["real"]

def test_format_prompt_raises_on_missing_variable(self):
"""A genuinely missing variable still raises a ValueError."""
template = StringPromptTemplate.from_template("Hello {name}")
with pytest.raises(ValueError, match="Missing required variables"):
template.format_prompt()
85 changes: 85 additions & 0 deletions tests/prompt/utils/test_fstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#
# Copyright 2026 The Dapr Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""Unit tests for f-string template variable extraction and rendering."""

from dapr_agents.prompt.utils.fstring import (
extract_fstring_variables,
render_fstring_template,
)


class TestExtractFstringVariables:
"""Tests for extract_fstring_variables()."""

def test_extracts_simple_variable(self):
"""A single placeholder is returned as a variable."""
assert extract_fstring_variables("Hello {name}") == ["name"]

def test_extracts_multiple_variables_in_sentence(self):
"""Placeholders embedded in text are all extracted, in order."""
assert extract_fstring_variables("Hi {name}, you are {age}") == [
"name",
"age",
]

def test_preserves_duplicate_variables(self):
"""Repeated placeholders are preserved (not de-duplicated)."""
assert extract_fstring_variables("{a} {b} {a}") == ["a", "b", "a"]

def test_ignores_escaped_braces(self):
"""Escaped braces ({{ and }}) are literal characters, not variables."""
assert extract_fstring_variables("{{literal}}") == []

def test_mixes_escaped_braces_and_real_variables(self):
"""A literal JSON example does not leak into the extracted variables."""
template = 'JSON: {{"k": "v"}} and {q}'
assert extract_fstring_variables(template) == ["q"]

def test_ignores_format_spec(self):
"""A format spec is dropped, matching str.format field names."""
assert extract_fstring_variables("{value:>10}") == ["value"]

def test_supports_attribute_and_index_access(self):
"""Attribute and index access field names are preserved."""
assert extract_fstring_variables("{obj.attr} {arr[0]}") == [
"obj.attr",
"arr[0]",
]

def test_returns_empty_for_no_placeholders(self):
"""Plain text without placeholders yields no variables."""
assert extract_fstring_variables("no placeholders here") == []


class TestRenderFstringTemplate:
"""Tests for render_fstring_template()."""

def test_renders_escaped_braces_as_literals(self):
"""Escaped braces render as literal braces and need no variable."""
template = 'Respond as JSON like {{"answer": "..."}}. Q: {question}'
assert (
render_fstring_template(template, question="What is 2+2?")
== 'Respond as JSON like {"answer": "..."}. Q: What is 2+2?'
)

def test_extract_and_render_agree_on_required_variables(self):
"""Every variable render needs is reported by extract, and no extras."""
template = 'Respond as JSON like {{"answer": "..."}}. Q: {question}'
variables = extract_fstring_variables(template)
assert variables == ["question"]
# Providing exactly the extracted variables renders without error.
rendered = render_fstring_template(
template, **{name: "x" for name in variables}
)
assert rendered == 'Respond as JSON like {"answer": "..."}. Q: x'
Loading