Skip to content

Python: ChatHistory.from_rendered_prompt decodes HTML entities twice, so a user message like "&" reaches the model as "&" #14619

Closed

Description

Description

ChatHistory.from_rendered_prompt decodes HTML entities twice. A user value that contains an entity like & reaches the model changed.

The template engine escapes the value once, so & becomes &. The XML parser then decodes it once to &. After that, html.unescape runs on the parsed text again and turns it into &.

This affects any prompt function with a <message> tag, because KernelFunctionFromPrompt uses from_rendered_prompt before it calls the chat service.

Reproduction

I ran this on current main (7a92cd4), Python 3.13.

import asyncio
from semantic_kernel import Kernel
from semantic_kernel.contents import ChatHistory
from semantic_kernel.functions import KernelArguments
from semantic_kernel.prompt_template import KernelPromptTemplate, PromptTemplateConfig

TEMPLATE = '<message role="system">You explain HTML.</message><message role="user">{{$input}}</message>'

async def main():
    template = KernelPromptTemplate(
        prompt_template_config=PromptTemplateConfig(name="t", template=TEMPLATE)
    )
    for text in ["What does &amp; mean in HTML?", "Write &lt;b&gt; as text", "Tom & Jerry <3"]:
        rendered = await template.render(Kernel(), KernelArguments(input=text))
        history = ChatHistory.from_rendered_prompt(rendered)
        got = history.messages[-1].content
        print("input   :", repr(text))
        print("rendered:", repr(rendered))
        print("message :", repr(got), "OK" if got == text else "CHANGED")

asyncio.run(main())

Output on main:

input   : 'What does &amp; mean in HTML?'
rendered: '<message role="system">You explain HTML.</message><message role="user">What does &amp;amp; mean in HTML?</message>'
message : 'What does & mean in HTML?' CHANGED
input   : 'Write &lt;b&gt; as text'
rendered: '<message role="system">You explain HTML.</message><message role="user">Write &amp;lt;b&amp;gt; as text</message>'
message : 'Write <b> as text' CHANGED
input   : 'Tom & Jerry <3'
rendered: '<message role="system">You explain HTML.</message><message role="user">Tom &amp; Jerry &lt;3</message>'
message : 'Tom & Jerry <3' OK

I also checked the real call path. I added a prompt function to a kernel with a fake chat service that records the chat history it receives. The input What does &amp; mean in HTML? arrives at the service as [('system', 'You explain HTML.'), ('user', 'What does & mean in HTML?')].

The same thing happens for text after a <message> tag and for a plain template with no tags. ChatHistory.to_prompt() followed by from_rendered_prompt() also changes any message that contains a literal entity.

Expected

The model receives exactly the text the user passed in, What does &amp; mean in HTML?.

Cause

The parsed element text is already decoded. The code calls unescape on it again in ChatHistory.from_rendered_prompt, ChatMessageContent.from_element, TextContent.from_element and ReasoningContent.from_element.

The second decode was probably added for CDATA sections. The parser keeps CDATA content literal, so an escaped value inside <![CDATA[...]]> needs one decode. I did not want to break that case.

Possible fix

I have a branch ready: https://github.com/VANDRANKI/semantic-kernel/tree/fix/rendered-prompt-double-unescape

It removes the extra unescape on parsed text and decodes CDATA sections once before parsing. It adds 16 tests. 12 of them fail on main and all pass on the branch. The CDATA tests pass on both, as a guard. The existing contents, prompt template, functions, template engine, kernel and agents unit tests pass. I can't open a pull request from this account right now, so the compare link is main...VANDRANKI:semantic-kernel:fix/rendered-prompt-double-unescape

Activity

  1. added a commit that references this issue on Oct 26, 2026
    cb59096
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pythonPull requests for the Python Semantic Kerneltriage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions