Bug description
Any analysis prompt that interpolates the transcript directly — {{ messages }} — sends the model the literal string <[]interface {} Value> instead of the conversation. The transcript never reaches the LLM.
Because the prompt is otherwise well-formed, the model does not error. It returns valid, correctly-shaped, confident JSON that is entirely invented. The Analysis tab populates, nothing is logged, nothing 500s, and the stored analysis is fiction.
This is model-independent — every provider behaves identically, because every provider receives the same empty prompt.
I suspect this is the underlying cause of #127, which was closed without a root cause. That report configures Parameters: Conversation → Messages → messages and sees an analysis that never appears meaningfully.
Root cause
utils.NormalizeInterface deliberately JSON-parses string arguments into Go composites:
case string:
if len(trimmed) > 0 && (trimmed[0] == '{' || trimmed[0] == '[') {
var parsed interface{}
if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
normalized[normalizedKey] = normalizeValue(parsed) // string -> []interface{}
break
}
}
The transcript arrives as a JSON string, so it becomes []interface{}. pkg/parsers/pongo2.template.parser.go then passes that straight to pongo2:
tpl.Execute(pongo2.Context(CanonicalizePromptArguments(utils.NormalizeInterface(argument))))
pongo2 has no string form for a composite value and falls back to Go's debug formatting, emitting <[]interface {} Value>.
So normalization is what breaks the value it exists to normalize. It converts a perfectly renderable JSON string into a type the template engine cannot render.
Composites are still needed for the tag form ({% for m in messages %}), which is presumably why the conversion exists — so this is specifically about the direct-interpolation path.
Steps to reproduce
Reproducible entirely from a unit test in the existing package, no infrastructure needed:
func TestRepro(t *testing.T) {
logger, _ := commons.NewApplicationLogger()
parser := NewPongo2StringTemplateParser(logger)
transcript := `[{"role":"user","content":"i want to book a cab"},` +
`{"role":"assistant","content":"sure, where to?"}]`
fmt.Println(parser.Parse("Analyse this conversation: {{ messages }}",
map[string]interface{}{"messages": transcript}))
fmt.Println(parser.Parse("{% for m in messages %}{{ m.role }}: {{ m.content }}\n{% endfor %}",
map[string]interface{}{"messages": transcript}))
}
Expected behavior
{{ messages }} should render the transcript in a form the model can read.
Actual behavior
=== TAGLESS RENDER ===
Analyse this conversation: <[]interface {} Value>
=== TAG RENDER ===
user: i want to book a cab
assistant: sure, where to?
The tag form works. Only direct interpolation is broken.
Proposed fix
JSON-encode composite values before rendering, but only where it is safe to do so:
- Skip templates containing
{% %} entirely — those iterate the composite themselves and need the original value.
- Encode only keys used as a standalone
{{ key }}. Attribute access such as {{ message.language }} must keep resolving against the live map, which the existing TestPongo2StringTemplateParser_Parse_DottedKeys covers.
- Mark the encoded payload safe, otherwise pongo2 autoescaping turns the JSON quotes into
".
A blanket "encode all composites" approach breaks both the dotted-key and the tag cases, so the narrowing is the substance of the fix.
Happy to open a PR — I have the change and tests passing locally. An alternative design would be a {{ messages|json }} filter plus a warning log on bare composite interpolation; that is non-breaking but requires every existing template to be edited. I went with the first approach but I am glad to switch if you prefer the explicit filter.
Rapida version
main
Deployment type
Docker
Bug description
Any analysis prompt that interpolates the transcript directly —
{{ messages }}— sends the model the literal string<[]interface {} Value>instead of the conversation. The transcript never reaches the LLM.Because the prompt is otherwise well-formed, the model does not error. It returns valid, correctly-shaped, confident JSON that is entirely invented. The Analysis tab populates, nothing is logged, nothing 500s, and the stored analysis is fiction.
This is model-independent — every provider behaves identically, because every provider receives the same empty prompt.
I suspect this is the underlying cause of #127, which was closed without a root cause. That report configures
Parameters: Conversation → Messages → messagesand sees an analysis that never appears meaningfully.Root cause
utils.NormalizeInterfacedeliberately JSON-parses string arguments into Go composites:The transcript arrives as a JSON string, so it becomes
[]interface{}.pkg/parsers/pongo2.template.parser.gothen passes that straight to pongo2:pongo2 has no string form for a composite value and falls back to Go's debug formatting, emitting
<[]interface {} Value>.So normalization is what breaks the value it exists to normalize. It converts a perfectly renderable JSON string into a type the template engine cannot render.
Composites are still needed for the tag form (
{% for m in messages %}), which is presumably why the conversion exists — so this is specifically about the direct-interpolation path.Steps to reproduce
Reproducible entirely from a unit test in the existing package, no infrastructure needed:
Expected behavior
{{ messages }}should render the transcript in a form the model can read.Actual behavior
The tag form works. Only direct interpolation is broken.
Proposed fix
JSON-encode composite values before rendering, but only where it is safe to do so:
{% %}entirely — those iterate the composite themselves and need the original value.{{ key }}. Attribute access such as{{ message.language }}must keep resolving against the live map, which the existingTestPongo2StringTemplateParser_Parse_DottedKeyscovers.".A blanket "encode all composites" approach breaks both the dotted-key and the tag cases, so the narrowing is the substance of the fix.
Happy to open a PR — I have the change and tests passing locally. An alternative design would be a
{{ messages|json }}filter plus a warning log on bare composite interpolation; that is non-breaking but requires every existing template to be edited. I went with the first approach but I am glad to switch if you prefer the explicit filter.Rapida version
mainDeployment type
Docker