0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
365 lines
10 KiB
Markdown
365 lines
10 KiB
Markdown
---
|
|
sidebar_label: Chat threads
|
|
sidebar_position: 32
|
|
title: Chat Conversations and Multi-Turn Threads
|
|
description: Configure chat conversations and multi-turn threads for LLM evaluation. Learn conversation history, multi-shot prompts, and chat flow testing.
|
|
keywords:
|
|
[
|
|
chat conversations,
|
|
multi-turn evaluation,
|
|
conversation history,
|
|
chat threads,
|
|
dialogue testing,
|
|
conversational AI,
|
|
chat flow,
|
|
]
|
|
pagination_prev: configuration/outputs
|
|
pagination_next: configuration/caching
|
|
---
|
|
|
|
# Chat conversations / threads
|
|
|
|
The [prompt file](/docs/configuration/prompts#file-based-prompts) supports a message in OpenAI's JSON prompt format. This allows you to set multiple messages including the system prompt. For example:
|
|
|
|
```json
|
|
[
|
|
{ "role": "system", "content": "You are a helpful assistant." },
|
|
{ "role": "user", "content": "Who won the world series in {{ year }}?" }
|
|
]
|
|
```
|
|
|
|
Equivalent yaml is also supported:
|
|
|
|
```yaml
|
|
- role: system
|
|
content: You are a helpful assistant.
|
|
- role: user
|
|
content: Who won the world series in {{ year }}?
|
|
```
|
|
|
|
## Multishot conversations
|
|
|
|
Most providers support full "multishot" chat conversations, including multiple assistant, user, and system prompts.
|
|
|
|
One way to do this, if you are using the OpenAI format, is by creating a list of `{role, content}` objects. Here's an example:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
prompts:
|
|
- file://prompt.json
|
|
|
|
providers:
|
|
- openai:gpt-5-mini
|
|
|
|
tests:
|
|
- vars:
|
|
messages:
|
|
- role: system
|
|
content: Respond as a pirate
|
|
- role: user
|
|
content: Who founded Facebook?
|
|
- role: assistant
|
|
content: Mark Zuckerberg
|
|
- role: user
|
|
content: Did he found any other companies?
|
|
```
|
|
|
|
Then the prompt itself is just a JSON dump of `messages`:
|
|
|
|
```liquid title="prompt.json"
|
|
{{ messages | dump }}
|
|
```
|
|
|
|
## Simplified chat markup
|
|
|
|
Alternatively, you may prefer to specify a list of `role: message`, like this:
|
|
|
|
```yaml
|
|
tests:
|
|
- vars:
|
|
messages:
|
|
- user: Who founded Facebook?
|
|
- assistant: Mark Zuckerberg
|
|
- user: Did he found any other companies?
|
|
```
|
|
|
|
This simplifies the config, but we need to work some magic in the prompt template:
|
|
|
|
```liquid title="prompt.json"
|
|
[
|
|
{% for message in messages %}
|
|
{% set outer_loop = loop %}
|
|
{% for role, content in message %}
|
|
{
|
|
"role": "{{ role }}",
|
|
"content": "{{ content }}"
|
|
}{% if not (loop.last and outer_loop.last) %},{% endif %}
|
|
{% endfor %}
|
|
{% endfor %}
|
|
]
|
|
```
|
|
|
|
## Creating a conversation history fixture
|
|
|
|
Using nunjucks templates, we can combine multiple chat messages. Here's an example in which the previous conversation is a fixture for _all_ tests. Each case tests a different follow-up message:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
# Set up the conversation history
|
|
defaultTest:
|
|
vars:
|
|
system_message: Answer concisely
|
|
messages:
|
|
- user: Who founded Facebook?
|
|
- assistant: Mark Zuckerberg
|
|
- user: What's his favorite food?
|
|
- assistant: Pizza
|
|
|
|
# Test multiple follow-ups
|
|
tests:
|
|
- vars:
|
|
question: Did he create any other companies?
|
|
- vars:
|
|
question: What is his role at Internet.org?
|
|
- vars:
|
|
question: Will he let me borrow $5?
|
|
```
|
|
|
|
In the prompt template, we construct the conversation history followed by a user message containing the `question`:
|
|
|
|
```liquid title="prompt.json"
|
|
[
|
|
{
|
|
"role": "system",
|
|
"content": {{ system_message | dump }}
|
|
},
|
|
{% for message in messages %}
|
|
{% for role, content in message %}
|
|
{
|
|
"role": "{{ role }}",
|
|
"content": {{ content | dump }}
|
|
},
|
|
{% endfor %}
|
|
{% endfor %}
|
|
{
|
|
"role": "user",
|
|
"content": {{ question | dump }}
|
|
}
|
|
]
|
|
```
|
|
|
|
:::info
|
|
Variables containing multiple lines and quotes are automatically escaped in JSON prompt files.
|
|
|
|
If the file is not valid JSON (such as in the case above, due to the nunjucks `{% for %}` loops), use the built-in nunjucks filter [`dump`](https://mozilla.github.io/nunjucks/templating.html#dump) to stringify the object as JSON.
|
|
:::
|
|
|
|
## Using the `_conversation` variable {#using-the-conversation-variable}
|
|
|
|
A built-in `_conversation` variable contains the full prompt and previous turns of a conversation. Use it to reference previous outputs and test an ongoing chat conversation.
|
|
|
|
The `_conversation` variable has the following type signature:
|
|
|
|
```ts
|
|
type Completion = {
|
|
prompt: string | object;
|
|
input: string;
|
|
output: string;
|
|
};
|
|
|
|
type Conversation = Completion[];
|
|
```
|
|
|
|
In most cases, you'll loop through the `_conversation` variable and use each `Completion` object.
|
|
|
|
Use `completion.prompt` to reference the previous conversation. For example, to get the number of messages in a chat-formatted prompt:
|
|
|
|
```
|
|
{{ completion.prompt.length }}
|
|
```
|
|
|
|
Or to get the first message in the conversation:
|
|
|
|
```
|
|
{{ completion.prompt[0] }}
|
|
```
|
|
|
|
Use `completion.input` as a shortcut to get the last user message. In a chat-formatted prompt, `input` is set to the last user message, equivalent to `completion.prompt[completion.prompt.length - 1].content`.
|
|
|
|
Here's an example test config. Note how each question assumes context from the previous output:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
tests:
|
|
- vars:
|
|
question: Who founded Facebook?
|
|
- vars:
|
|
question: Where does he live?
|
|
- vars:
|
|
question: Which state is that in?
|
|
```
|
|
|
|
Here is the corresponding prompt:
|
|
|
|
```json title="prompt.json"
|
|
[
|
|
// highlight-start
|
|
{% for completion in _conversation %}
|
|
{
|
|
"role": "user",
|
|
"content": "{{ completion.input }}"
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "{{ completion.output }}"
|
|
},
|
|
{% endfor %}
|
|
// highlight-end
|
|
{
|
|
"role": "user",
|
|
"content": "{{ question }}"
|
|
}
|
|
]
|
|
```
|
|
|
|
The prompt inserts the previous conversation into the test case, creating a full turn-by-turn conversation:
|
|
|
|

|
|
|
|
Try it yourself by using the [full example config](https://github.com/promptfoo/promptfoo/tree/main/examples/config-multi-turn).
|
|
|
|
:::info
|
|
When a prompt references `_conversation` as a Nunjucks variable, the eval will run single-threaded (concurrency of 1).
|
|
:::
|
|
|
|
## Separating Chat Conversations
|
|
|
|
Each unique `conversationId` maintains its own separate conversation history. Scenarios automatically isolate conversations by default.
|
|
|
|
You can explicitly control conversation grouping by adding a `conversationId` to the test metadata:
|
|
|
|
```yaml
|
|
tests:
|
|
- vars:
|
|
question: 'Who founded Facebook?'
|
|
metadata:
|
|
conversationId: 'conversation1'
|
|
- vars:
|
|
question: 'Where does he live?'
|
|
metadata:
|
|
conversationId: 'conversation1'
|
|
- vars:
|
|
question: 'Where is Yosemite National Park?'
|
|
metadata:
|
|
conversationId: 'conversation2'
|
|
- vars:
|
|
question: 'What are good hikes there?'
|
|
metadata:
|
|
conversationId: 'conversation2'
|
|
```
|
|
|
|
### Including JSON in prompt content
|
|
|
|
In some cases, you may want to send JSON _within_ the OpenAI `content` field. In order to do this, you must ensure that the JSON is properly escaped.
|
|
|
|
Here's an example that prompts OpenAI with a JSON object of the structure `{query: string, history: {reply: string}[]}`. It first constructs this JSON object as the `input` variable. Then, it includes `input` in the prompt with proper JSON escaping:
|
|
|
|
```json title="prompt.json"
|
|
{% set input %}
|
|
{
|
|
"query": "{{ query }}",
|
|
"history": [
|
|
{% for completion in _conversation %}
|
|
{"reply": "{{ completion.output }}"} {% if not loop.last %},{% endif %}
|
|
{% endfor %}
|
|
]
|
|
}
|
|
{% endset %}
|
|
|
|
[{
|
|
"role": "user",
|
|
"content": {{ input | trim | dump }}
|
|
}]
|
|
```
|
|
|
|
Here's the associated config:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
prompts:
|
|
- file://prompt.json
|
|
providers:
|
|
- openai:gpt-5-mini
|
|
tests:
|
|
- vars:
|
|
query: how you doing
|
|
- vars:
|
|
query: need help with my passport
|
|
```
|
|
|
|
This has the effect of including the conversation history _within_ the prompt content. Here's what's sent to OpenAI for the second test case:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"role": "user",
|
|
"content": "{\n \"query\": \"how you doing\",\n \"history\": [\n \n ]\n}"
|
|
}
|
|
]
|
|
```
|
|
|
|
## Using `storeOutputAs`
|
|
|
|
The `storeOutputAs` option makes it possible to reference previous outputs in multi-turn conversations. When set, it records the LLM output as a variable that can be used in subsequent chats.
|
|
|
|
Here's an example:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
prompts:
|
|
- 'Respond to the user: {{message}}'
|
|
|
|
providers:
|
|
- openai:gpt-5
|
|
|
|
tests:
|
|
- vars:
|
|
message: "What's your favorite fruit? You must pick one. Output the name of a fruit only"
|
|
options:
|
|
storeOutputAs: favoriteFruit
|
|
- vars:
|
|
message: 'Why do you like {{favoriteFruit}} so much?'
|
|
options:
|
|
storeOutputAs: reason
|
|
- vars:
|
|
message: 'Write a snarky 2 sentence rebuttal to this argument for loving {{favoriteFruit}}: \"{{reason}}\"'
|
|
```
|
|
|
|
This creates `favoriteFruit` and `reason` vars on-the-go, as the chatbot answers questions.
|
|
|
|
### Manipulating outputs with `transform`
|
|
|
|
Outputs can be modified before storage using the `transform` property:
|
|
|
|
```yaml title="promptfooconfig.yaml"
|
|
tests:
|
|
- vars:
|
|
message: "What's your favorite fruit? You must pick one. Output the name of a fruit only"
|
|
options:
|
|
storeOutputAs: favoriteFruit
|
|
// highlight-start
|
|
transform: output.split(' ')[0]
|
|
// highlight-end
|
|
- vars:
|
|
message: "Why do you like {{favoriteFruit}} so much?"
|
|
options:
|
|
storeOutputAs: reason
|
|
- vars:
|
|
message: 'Write a snarky 2 sentence rebuttal to this argument for loving {{favoriteFruit}}: \"{{reason}}\"'
|
|
```
|
|
|
|
Transforms can be Javascript snippets or they can be entire separate Python or Javascript files. See [docs on transform](/docs/configuration/guide/#transforming-outputs).
|
|
|
|
## See Also
|
|
|
|
- [Prompt Parameters](/docs/configuration/prompts) - Learn about different ways to define prompts
|
|
- [Test Configuration](/docs/configuration/guide) - Complete guide to setting up test configurations
|
|
- [Transformer Functions](/docs/configuration/guide/#transforming-outputs) - How to transform outputs between test cases
|
|
- [Nunjucks Templates](https://mozilla.github.io/nunjucks/templating.html) - Documentation for the template language used in prompt files
|
|
- [Multi-turn Conversation Example](https://github.com/promptfoo/promptfoo/tree/main/examples/config-multi-turn) - Complete example of multi-turn conversations
|