> ## Documentation Index
> Fetch the complete documentation index at: https://www.meilisearch.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a chat interface

> Build a multi-turn conversational search interface using Meilisearch's chat completions API.

Meilisearch's chat completions endpoint works as a built-in RAG (Retrieval Augmented Generation) system: for each user message, Meilisearch searches your indexes, then passes the retrieved documents to the LLM to generate a grounded response. This guide shows you how to build a multi-turn chat interface on top of this.

Make sure you have completed the [setup guide](/capabilities/conversational_search/getting_started/setup) before continuing.

<Note>
  In code examples, replace `WORKSPACE_NAME` with the name of your workspace. On Meilisearch Cloud, the default workspace name is `cloud`.
</Note>

## Streaming is required

All requests to the chat completions endpoint must include `"stream": true`. Non-streaming (`stream: false`) is not yet supported and returns a `501 Not Implemented` error.

## Message roles

Every entry in the `messages` array carries a `role` that tells the LLM who authored it. The chat completions endpoint uses three roles, each with a distinct origin:

| Role        | Origin       | Typical content                                                                                                                                                                          |
| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `system`    | Meilisearch  | Workspace-level instructions (the `prompts.system` string) plus internal tool descriptions injected by Meilisearch. You do not need to send this role yourself; Meilisearch prepends it. |
| `assistant` | LLM provider | Responses generated by the configured model, including tool calls. Push these back into `messages` to preserve context on follow-up turns.                                               |
| `user`      | User input   | Questions and follow-ups coming from the end user of your application. This is what you add to `messages` before each request.                                                           |

Understanding the origin matters when debugging: unexpected `system` content usually means the workspace system prompt needs tuning, wrong answers are an `assistant` problem, and malformed input is a `user` problem.

## Send a streaming request

Send a `POST` request to `/chats/{workspace}/chat/completions` with `stream: true`:

<CodeGroup>
  ```bash cURL theme={null}
  curl -N \
    -X POST 'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions' \
    -H 'Authorization: Bearer MEILISEARCH_KEY' \
    -H 'Content-Type: application/json' \
    --data-binary '{
      "model": "PROVIDER_MODEL_UID",
      "stream": true,
      "messages": [
        {
          "role": "user",
          "content": "What movies are about artificial intelligence?"
        }
      ]
    }'
  ```

  ```javascript OpenAI SDK theme={null}
  import OpenAI from 'openai';

  const client = new OpenAI({
    baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
    apiKey: 'MEILISEARCH_KEY',
  });

  const stream = await client.chat.completions.create({
    model: 'PROVIDER_MODEL_UID',
    stream: true,
    messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    process.stdout.write(content);
  }
  ```

  ```javascript Vercel AI SDK theme={null}
  import { createOpenAI } from '@ai-sdk/openai';
  import { streamText } from 'ai';

  const meilisearch = createOpenAI({
    baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
    apiKey: 'MEILISEARCH_KEY',
  });

  const { textStream } = streamText({
    model: meilisearch('PROVIDER_MODEL_UID'),
    messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
  });

  for await (const text of textStream) {
    process.stdout.write(text);
  }
  ```
</CodeGroup>

This basic request works and the LLM will search your indexes and generate an answer. However, without Meilisearch tools, you get no visibility into what is being searched and no way to maintain conversation context across follow-up questions. The next section explains how to address this.

## Meilisearch tools

Meilisearch provides three special tools that improve the chat experience. Declare them in the `tools` array of your request and Meilisearch intercepts them — they are never forwarded to the LLM provider.

<Warning>
  These tool definitions must include the exact parameter schemas below. Missing or incorrect parameters will prevent the tools from working.
</Warning>

| Tool                              | Purpose                                                                               |
| --------------------------------- | ------------------------------------------------------------------------------------- |
| `_meiliSearchProgress`            | Reports what searches are being performed in real time                                |
| `_meiliSearchSources`             | Returns the documents used by the LLM to formulate its answer                         |
| `_meiliAppendConversationMessage` | Asks the client to append internal tool calls and results to the conversation history |

The `call_id` field links `_meiliSearchProgress` and `_meiliSearchSources` events together: both carry the same `call_id`, allowing you to associate a search query with the documents it returned.

`_meiliAppendConversationMessage` is the key to multi-turn conversations. Since the endpoint is stateless, Meilisearch uses this tool to expose the internal search tool calls and their results back to the client. You must push these messages into your `messages` array before the next request, or the LLM will lose the context from previous searches and produce lower-quality answers.

### Tool schemas

<CodeGroup>
  ```json _meiliSearchProgress theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliSearchProgress",
      "description": "Provides information about the current Meilisearch search operation",
      "parameters": {
        "type": "object",
        "properties": {
          "call_id": {
            "type": "string",
            "description": "The call ID to track the sources of the search"
          },
          "function_name": {
            "type": "string",
            "description": "The name of the function being executed"
          },
          "function_parameters": {
            "type": "string",
            "description": "The parameters of the function being executed, encoded in JSON"
          }
        },
        "required": ["call_id", "function_name", "function_parameters"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```

  ```json _meiliSearchSources theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliSearchSources",
      "description": "Provides sources of the search",
      "parameters": {
        "type": "object",
        "properties": {
          "call_id": {
            "type": "string",
            "description": "The call ID to track the original search associated to those sources"
          },
          "documents": {
            "type": "array",
            "items": { "type": "object" },
            "description": "The documents associated with the search. Only displayed attributes are returned"
          }
        },
        "required": ["call_id", "documents"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```

  ```json _meiliAppendConversationMessage theme={null}
  {
    "type": "function",
    "function": {
      "name": "_meiliAppendConversationMessage",
      "description": "Append a new message to the conversation based on what happened internally",
      "parameters": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "description": "The role of the message author"
          },
          "content": {
            "type": "string",
            "description": "The content of the message. Required unless tool_calls is specified"
          },
          "tool_calls": {
            "type": ["array", "null"],
            "description": "Tool calls generated by the model",
            "items": {
              "type": "object",
              "properties": {
                "id": { "type": "string" },
                "type": { "type": "string" },
                "function": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string" },
                    "arguments": { "type": "string" }
                  }
                }
              }
            }
          },
          "tool_call_id": {
            "type": ["string", "null"],
            "description": "Tool call this message is responding to"
          }
        },
        "required": ["role", "content", "tool_calls", "tool_call_id"],
        "additionalProperties": false
      },
      "strict": true
    }
  }
  ```
</CodeGroup>

## Complete example: progress, sources, and history

The following example combines all three tools and demonstrates the full recommended usage: streaming progress, displaying sources, and maintaining conversation history for multi-turn questions.

```javascript JavaScript (Fetch) theme={null}
const MEILISEARCH_TOOLS = [
  {
    type: 'function',
    function: {
      name: '_meiliSearchProgress',
      description: 'Provides information about the current Meilisearch search operation',
      parameters: {
        type: 'object',
        properties: {
          call_id: { type: 'string' },
          function_name: { type: 'string' },
          function_parameters: { type: 'string' },
        },
        required: ['call_id', 'function_name', 'function_parameters'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
  {
    type: 'function',
    function: {
      name: '_meiliSearchSources',
      description: 'Provides sources of the search',
      parameters: {
        type: 'object',
        properties: {
          call_id: { type: 'string' },
          documents: { type: 'array', items: { type: 'object' } },
        },
        required: ['call_id', 'documents'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
  {
    type: 'function',
    function: {
      name: '_meiliAppendConversationMessage',
      description: 'Append a new message to the conversation based on what happened internally',
      parameters: {
        type: 'object',
        properties: {
          role: { type: 'string' },
          content: { type: 'string' },
          tool_calls: { type: ['array', 'null'] },
          tool_call_id: { type: ['string', 'null'] },
        },
        required: ['role', 'content', 'tool_calls', 'tool_call_id'],
        additionalProperties: false,
      },
      strict: true,
    },
  },
];

const messages = [];

async function chat(userMessage) {
  messages.push({ role: 'user', content: userMessage });

  const response = await fetch('MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer MEILISEARCH_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'PROVIDER_MODEL_UID',
      stream: true,
      messages,
      tools: MEILISEARCH_TOOLS,
    }),
  });

  const reader = response.body?.getReader();
  if (!reader) throw new Error('No readable stream on response');
  const decoder = new TextDecoder();
  let answer = '';
  let buffer = '';
  const pendingToolCalls = {};

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() ?? ''; // retain any incomplete trailing line

    for (const line of lines) {
      if (!line.startsWith('data: ') || line === 'data: [DONE]') continue;

      const chunk = JSON.parse(line.slice(6));
      const delta = chunk.choices[0]?.delta;

      // Accumulate answer tokens
      if (delta?.content) {
        answer += delta.content;
        process.stdout.write(delta.content);
      }

      // Accumulate tool call arguments (they may arrive in multiple chunks)
      for (const toolCall of delta?.tool_calls ?? []) {
        if (toolCall.id) {
          pendingToolCalls[toolCall.id] = { name: toolCall.function.name, args: '' };
        }
        const pending = toolCall.id
          ? pendingToolCalls[toolCall.id]
          : Object.values(pendingToolCalls).at(-1);
        if (pending && toolCall.function?.arguments) {
          pending.args += toolCall.function.arguments;
        }
      }
    }
  }

  // Process completed tool calls
  for (const call of Object.values(pendingToolCalls)) {
    const args = JSON.parse(call.args);

    if (call.name === '_meiliSearchProgress') {
      // Show real-time search progress in the UI
      const params = JSON.parse(args.function_parameters);
      console.log(`Searched "${params.q}" in index "${params.index_uid}"`);
    }

    if (call.name === '_meiliSearchSources') {
      // Display source documents alongside the answer
      console.log('Sources used:', args.documents);
    }

    if (call.name === '_meiliAppendConversationMessage') {
      // Append internal search context to maintain quality in follow-up questions
      messages.push(args);
    }
  }

  messages.push({ role: 'assistant', content: answer });
}

// First question
await chat('What movies are about artificial intelligence?');
// Follow-up — the agent uses the search context from the previous turn
await chat('Which one has the best reviews?');
```

## Troubleshooting

### Empty reply from server (curl error 52)

**Causes:**

* Chat completions feature not enabled
* Missing authentication in requests

**Solution:**

1. Enable the feature (see [setup guide](/capabilities/conversational_search/getting_started/setup))
2. Include the `Authorization` header in all requests

### "Invalid API key" error

**Cause:** Using the wrong type of API key

**Solution:**

* Use the "Default Chat API Key"
* Do not use search or admin API keys for chat endpoints
* Find your chat key with the [list keys endpoint](/reference/api/keys/list-api-keys)

### "Socket connection closed unexpectedly"

**Cause:** Usually means the LLM provider API key is missing or invalid in workspace settings

**Solution:**

1. Check workspace configuration:

   <CodeGroup>
     ```bash cURL theme={null}
     curl \
       -X GET 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
       -H "Authorization: Bearer MEILISEARCH_KEY"
     ```
   </CodeGroup>

2. Update with a valid API key:

   <CodeGroup>
     ```bash cURL theme={null}
     curl \
       -X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
       -H "Authorization: Bearer MEILISEARCH_KEY" \
       -H "Content-Type: application/json" \
       --data-binary '{ "apiKey": "your-valid-api-key" }'
     ```
   </CodeGroup>

### No search progress visible

**Cause:** The `_meiliSearchProgress` tool is not declared in the request

**Solution:**

The search still runs and the LLM still answers, but without `_meiliSearchProgress` you receive no visibility into what searches are being performed. Add all three Meilisearch tools to your request as shown in the [complete example](#complete-example-progress-sources-and-history).

## Next steps

<CardGroup cols={2}>
  <Card title="One-shot summarization" href="/capabilities/conversational_search/getting_started/one_shot_summarization">
    Generate single AI answers without conversation history.
  </Card>

  <Card title="Stream chat responses" href="/capabilities/conversational_search/how_to/stream_chat_responses">
    Handle streaming responses for a real-time experience.
  </Card>

  <Card title="Display source documents" href="/capabilities/conversational_search/how_to/display_source_documents">
    Show users which documents were used to generate responses.
  </Card>

  <Card title="Configure guardrails" href="/capabilities/conversational_search/how_to/configure_guardrails">
    Restrict AI responses to topics covered by your data.
  </Card>

  <Card title="Chat completions API reference" href="/reference/api/chats/request-a-chat-completion">
    Full reference for the chat completions endpoint.
  </Card>

  <Card title="Reduce hallucination" href="/capabilities/conversational_search/advanced/reduce_hallucination">
    Techniques to keep AI responses grounded in your data.
  </Card>
</CardGroup>
