Skip to content

[Bug]: analysis prompts render the transcript as <[]interface {} Value>, so the model analyses nothing #207

Description

@Manibharadwaj

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 &quot;.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions