Skip to content

fix(prompt): handle escaped braces in f-string variable extraction - #697

Open
anxkhn wants to merge 6 commits into
dapr:mainfrom
anxkhn:fix/fstring-escaped-braces
Open

fix(prompt): handle escaped braces in f-string variable extraction#697
anxkhn wants to merge 6 commits into
dapr:mainfrom
anxkhn:fix/fstring-escaped-braces

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
`extract_fstring_variables` (`dapr_agents/prompt/utils/fstring.py`) found template
placeholders with the regex `\{([^{}]+)\}`. That regex is unaware of `str.format`
escaped braces: in an f-string / `str.format` template, `{{` and `}}` are literal
`{` and `}`, but the regex matched the text between them as a variable. For a
template such as:

    Respond as JSON like {{"answer": "..."}}. Q: {question}

it returned the variables `['"answer": "..."', 'question']` instead of just
`['question']`.

This disagreed with `render_fstring_template`, which calls `template.format(**kwargs)`
and correctly renders `{{ }}` as literal braces. As a result,
`StringPromptTemplate.format_prompt` (`dapr_agents/prompt/string.py`) raised a
spurious `ValueError: Missing required variables in template: ['"answer": "..."']`,
because that phantom variable is not a valid identifier and can never be passed as a
keyword argument. Escaped braces are the only way to emit a literal `{`/`}` in a
template, and literal JSON examples in prompts (showing a model the expected output
shape) are a very common pattern, so valid templates were being rejected.

**Fix:** replace the regex with the standard library `string.Formatter().parse()`,
which shares `str.format()` semantics, so extraction now agrees with rendering.
Escaped braces are ignored, format specs are dropped (`{value:>10}` -> `value`),
and duplicates and attribute/index access (`{obj.attr}`, `{arr[0]}`) are preserved.
`render_fstring_template` is unchanged. A genuinely missing variable still raises the
same `ValueError` as before.

The change is contained to `extract_fstring_variables`; only the extraction body is
touched (swapping `import re` for `from string import Formatter`).

## Issue reference

We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation.

Please reference the issue this PR closes: #_[issue number]_

## Checklist

Please make sure you've completed the relevant tasks for this PR, out of the following list:

* [x] Created/updated tests
* [ ] Tested this change against all the quickstarts
* [x] Extended the documentation
    * [ ] Created the [dapr/docs](https://github.com/dapr/docs) PR: N/A - internal parsing-correctness fix, no public API / feature / config / documented-behavior change

## AGENTS.md Notes

`AGENTS.md` and its pre-PR gate were accurate and sufficient for this change. Two
small suggestions from working through this fix:

- The gate section could note that `ruff format` (no `--check`) rewrites files in
  place, whereas CI effectively checks formatting; using `uv run ruff format --check`
  locally mirrors CI without modifying the tree.
- The "License Header" rule is clear for new `.py` files. It could add a one-line
  pointer that `tests/` mirrors `dapr_agents/` (so a new test lands at the mirrored
  path), which is currently implied by the Testing section but easy to miss.

Notes on the checklist choices (not part of the PR body)

  • "Tested against all quickstarts": left unchecked. Quickstarts are E2E and need API
    keys + a Dapr sidecar; not run here. This is an internal parsing fix with unit
    coverage, so honestly leaving it unchecked is correct.
  • "Extended the documentation" is checked because the change is documented in-code
    (docstring/comment on extract_fstring_variables) and in this PR; the nested
    dapr/docs PR box is intentionally left unchecked with an inline N/A because
    AGENTS.md and the repo's PR rules exempt internal-only bug fixes that do not change
    the public API / features / config / observable behavior. If a reviewer disagrees,
    a two-line note in the f-string prompt docs is the fallback.

Changeset summary (what Anas is approving)

  • dapr_agents/prompt/utils/fstring.py (+10/-5): drop import re; extract variables
    with string.Formatter().parse(). Only extract_fstring_variables changes;
    render_fstring_template untouched.
  • tests/prompt/utils/test_fstring.py (new, +85): unit tests for extract + render
    (escaped braces, format spec, dupes, attr/index, plain text; render/extract agree).
  • tests/prompt/test_string_prompt.py (new, +50): end-to-end format_prompt tests
    (escaped-brace template renders; input_variables exclude escaped content; missing
    variable still raises).
  • Total: 3 files, +145/-5. Both new files carry the verbatim Apache 2.0 2026 header.

Verification (re-confirmed at draft time, 2026-07-04):

  • uv run --no-sync ruff format --check (3 changed files) -> "3 files already formatted".
  • uv run --no-sync flake8 ... --ignore=E501,F401,W503,E203,E704 -> exit 0.
  • Targeted pytest tests/prompt/utils/test_fstring.py tests/prompt/test_string_prompt.py
    -> 14 passed.
  • Verify phase (2026-07-03) ran the full gate GREEN: mypy Success (205 files),
    pytest tests -m "not integration" -> 952 passed / 15 skipped / 0 failed, and
    independently proved red->green (revert fstring.py only -> exactly 6 targeted tests fail).

extract_fstring_variables used the regex \{([^{}]+)\} to find template
placeholders, which is unaware of str.format escaped braces. In an
f-string/str.format template, {{ and }} are literal { and }, but the
regex matched the text between them as a variable. A template such as
'Respond as JSON like {{"answer": "..."}}. Q: {question}' was reported
as having the variables ['"answer": "..."', 'question'] instead of just
['question'].

This disagreed with render_fstring_template, which calls
template.format(**kwargs) and correctly renders {{ }} as literal braces.
StringPromptTemplate.format_prompt then raised a spurious
"Missing required variables" ValueError, because the phantom variable is
never a valid kwarg. Literal JSON examples in prompts are a very common
pattern, so valid templates were rejected.

Replace the regex with string.Formatter().parse(), which shares
str.format() semantics, so extraction and rendering agree. Escaped
braces are ignored, format specs are dropped ({x:>10} -> 'x'), and
duplicates and attribute/index access are preserved. Add unit tests for
extract_fstring_variables and StringPromptTemplate.format_prompt covering
escaped braces, format specs, and the existing behavior.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@anxkhn
anxkhn requested review from a team as code owners July 13, 2026 17:05
@sicoyle
sicoyle requested a review from Copilot July 14, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes f-string/str.format template variable extraction so escaped braces ({{ / }}) are treated as literals (matching str.format semantics), preventing StringPromptTemplate from rejecting valid prompt templates that include literal JSON examples.

Changes:

  • Replaced regex-based placeholder extraction with string.Formatter().parse() in extract_fstring_variables.
  • Added unit tests covering escaped braces, format specs, duplicates, and extraction/render agreement.
  • Added end-to-end tests verifying StringPromptTemplate.format_prompt accepts escaped-brace templates and still errors on genuinely missing variables.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
dapr_agents/prompt/utils/fstring.py Switches variable extraction to stdlib Formatter.parse() to properly ignore escaped braces and drop format specs.
tests/prompt/utils/test_fstring.py Adds focused unit coverage for f-string extraction/rendering (including escaped braces).
tests/prompt/test_string_prompt.py Adds E2E coverage for StringPromptTemplate behavior with escaped braces and missing variables.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dapr_agents/prompt/utils/fstring.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants