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

# Compare products in chat

> Create comparison configurations in Agent Studio so users can select products on your storefront and compare them in chat.

Product comparison lets users select products on your storefront and compare them with an Agent Studio agent.
The agent responds in the chat with a comparison table based on the selected records.

Comparison configurations control how the agent compares.
Each configuration defines the comparison goal, instructions, selection limits, and whether the configuration is enabled.

## Before you begin

* You need an Algolia application with the Agent Studio feature and the **Comparison** component enabled.
* To preview a configuration, you need an agent with an [Algolia Search tool](https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/tools/algolia-search.md) connected to an index.
* To integrate a configuration, your storefront needs the InstantSearch [`chat`](https://www.algolia.com/doc/api-reference/widgets/chat/js) widget.

## How product comparison works

1. Users select products from the search results with a **Compare** toggle on each hit.
   The comparison bar shows the selected products.
2. When users select **Compare**, the comparison bar opens the chat and sends a comparison request that names the configuration.
   The selected records travel with the request.
3. Agent Studio replaces the request with the instructions from the configuration before calling the model.
   The chat doesn't show a user message for the request: users go straight from the **Compare** button to the agent's answer, and the comparison bar clears the selection.
4. The default instructions tell the agent to compare the selected records with the `algolia_compare_products` tool.
   The table has one column per selected product and one row per comparison criterion.
   The values come from the product records.
   Missing values are marked as "not listed".

The comparison widgets require a configuration ID.
Create and enable a configuration before you add them to your storefront.

## Create a comparison configuration

**1. Open the comparison configurations list**

In the [Agent Studio dashboard](https://dashboard.algolia.com/generativeAi/agent-studio/agents), open your agent, go to the **Components** section, and select the **Comparison** component.

**2. Create a configuration**

Select **Create** and fill in:

* **Name**. An internal label. It's not used in the integration.
* **Goal**. What the comparison should help users do.
* **Instructions**. Extra guidance for the agent, such as which attributes matter most.

| Goal | What the agent does |
| - | - |
| Decision-relevant compare | Compares the attributes that drive a purchase decision, including differences from the descriptions, and ends with a one-line recommendation |
| Spec sheet | Lays out the structured attributes as a factual spec sheet, without opinions or a recommendation |
| Custom | Follows your instructions |

Choosing a goal replaces the **Instructions** text.
Editing the instructions switches the goal to **Custom**.

## Configure the selection limits

In the configuration editor, adjust how many products users can compare at once:

* **Minimum products**. The minimum number of products users must select before they can compare. The default is 2.
* **Maximum products**. The maximum number of products users can select. The default is 3. The maximum is 6.

Keep the maximum low so users can review the comparison.
If the maximum and minimum limits conflict,
the editor adjusts the other limit to match the one you changed.

Pass the same limits to the comparison widget to apply them on the storefront.

## Customize the system prompt

The **Advanced** section shows the system prompt sent to the model.
The default prompt tells the agent to compare the selected records with the `algolia_compare_products` tool, take every value from the records, and mark missing values as "not listed".

To write your own system prompt, turn on **Customize** and edit the text.
Keep the instructions to render the table with `algolia_compare_products`, take the values from the selected records, and mark missing values as "not listed".

## Preview a comparison

Use the live preview to test a configuration before you enable it:

1. Select an **Index** connected to your agent through an Algolia Search tool.
2. Use **Add product** to search for records and add them to the selection, within the configured limits.
3. Select **Run comparison**.

The preview opens a chat with your draft agent and runs the comparison against the current draft of the configuration.

## Enable and integrate a configuration

**1. Enable the configuration**

Turn on the **Enabled** toggle and save.
The agent only applies a configuration to compare requests once the configuration is enabled and saved.

**2. Copy the configuration ID**

Copy the **Configuration ID** from the editor.
Configuration IDs are stable: you can change the configuration's behavior without updating your integration.

**3. Add the widgets to your storefront**

Select **Integrate**, choose an integration option, and copy the generated code.
In InstantSearch, pass the configuration ID and selection limits to `CompareBar` (React) or `compareBar` (JavaScript), next to your hits and chat widgets.
The comparison bar shows the selected products but doesn't render a way to select them:
add a **Compare** toggle to each hit that reads the shared selection from the render state.

```jsx React expandable theme={"system"}
import {
  Chat,
  CompareBar,
  Hits,
  useInstantSearch,
} from 'react-instantsearch';

function Hit({ hit }) {
  const { indexRenderState } = useInstantSearch();
  const { isSelected, toggleItem, canAddItems } =
    indexRenderState.compare ?? {};
  const selected = isSelected?.(hit.objectID) ?? false;

  return (
    <article>
      <h2>{hit.name}</h2>
      <label>
        <input
          type="checkbox"
          checked={selected}
          disabled={!toggleItem || (!selected && !canAddItems)}
          onChange={() => toggleItem?.(hit)}
        />
        Compare
      </label>
    </article>
  );
}

<Hits hitComponent={Hit} />
<CompareBar
  minItems={2}
  maxItems={3}
  configurationId="CONFIGURATION_ID"
/>
<Chat agentId="AGENT_ID" />
```

```js JavaScript expandable theme={"system"}
import { chat, compareBar, hits } from 'instantsearch.js/es/widgets';

search.addWidgets([
  hits({
    container: '#hits',
    templates: {
      item(hit, { html }) {
        // Replace `instant_search` with your index name.
        const { isSelected, toggleItem, canAddItems } =
          search.renderState.instant_search?.compare ?? {};
        const selected = isSelected?.(hit.objectID) ?? false;

        return html`
          <article>
            <h2>${hit.name}</h2>
            <label>
              <input
                type="checkbox"
                checked=${selected}
                disabled=${!toggleItem || (!selected && !canAddItems)}
                onChange=${() => toggleItem?.(hit)}
              />
              Compare
            </label>
          </article>
        `;
      },
    },
  }),
  compareBar({
    container: '#compare-bar',
    minItems: 2,
    maxItems: 3,
    configurationId: 'CONFIGURATION_ID',
  }),
  chat({
    container: '#chat',
    agentId: 'AGENT_ID',
  }),
]);
```

For a custom comparison bar or toggle, use the `useCompare` Hook (React) or the `connectCompare` connector (JavaScript).
For examples and configuration options, see the comparison widget reference for [React](https://www.algolia.com/doc/api-reference/widgets/compare-bar/react.md) or [JavaScript](https://www.algolia.com/doc/api-reference/widgets/compare-bar/js.md).

## Compare from a single button

On pages without search results, such as the home page or a product page, you can start a comparison from one **Compare products** button.
The button opens a dialog with a regular search interface: a search box, hits with a **Compare** toggle, and a **Compare** button.
When users select **Compare** in the dialog, the dialog closes and the chat opens with the comparison.

The pattern uses the same widgets as a results page, inside a [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) element:

* The search box and hits inside the dialog belong to the same `InstantSearch` instance and index as the chat widget, so the comparison request reaches the chat.
* The **Compare** button in the dialog comes from `useCompare` (React) or `connectCompare` (JavaScript).
  Their `compare()` function returns `true` when it sends the request, which is when to close the dialog.
* Keep the widgets in the dialog mounted while it's closed, so the chat widget has an entry point when it starts.
  InstantSearch runs an initial search for the hidden hits.

The selection is empty every time the dialog opens, because the comparison bar clears it after each request.

```jsx React expandable theme={"system"}
import { useRef } from 'react';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
  Chat,
  Hits,
  InstantSearch,
  SearchBox,
  useCompare,
  useInstantSearch,
} from 'react-instantsearch';
import 'instantsearch.css/components/chat.css';

const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');

function Hit({ hit }) {
  const { indexRenderState } = useInstantSearch();
  const { isSelected, toggleItem, canAddItems } =
    indexRenderState.compare ?? {};
  const selected = isSelected?.(hit.objectID) ?? false;

  return (
    <article>
      <h2>{hit.name}</h2>
      <label>
        <input
          type="checkbox"
          checked={selected}
          disabled={!toggleItem || (!selected && !canAddItems)}
          onChange={() => toggleItem?.(hit)}
        />
        Compare
      </label>
    </article>
  );
}

function CompareDialog({ dialogRef }) {
  const { items, canCompare, maxItems, compare, clearItems } = useCompare({
    minItems: 2,
    maxItems: 3,
    configurationId: 'CONFIGURATION_ID',
  });

  function onCompare() {
    // `compare()` sends the request and clears the selection.
    // It returns `false` when the chat is busy or the selection is too small.
    if (compare()) {
      dialogRef.current.close();
    }
  }

  function onClose() {
    clearItems();
    dialogRef.current.close();
  }

  return (
    <dialog ref={dialogRef} aria-label="Compare products">
      <SearchBox />
      <Hits hitComponent={Hit} />
      <footer>
        <span>
          {items.length} of {maxItems} selected
        </span>
        <button type="button" onClick={onClose}>
          Cancel
        </button>
        <button type="button" disabled={!canCompare} onClick={onCompare}>
          Compare {items.length}
        </button>
      </footer>
    </dialog>
  );
}

function App() {
  const dialogRef = useRef(null);

  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <button type="button" onClick={() => dialogRef.current.showModal()}>
        Compare products
      </button>
      <CompareDialog dialogRef={dialogRef} />
      <Chat agentId="AGENT_ID" />
    </InstantSearch>
  );
}
```

```js JavaScript expandable theme={"system"}
import instantsearch from 'instantsearch.js';
import { connectCompare } from 'instantsearch.js/es/connectors';
import { chat, hits, searchBox } from 'instantsearch.js/es/widgets';
import 'instantsearch.css/components/chat.css';

/*
<button type="button" id="open-compare">Compare products</button>
<dialog id="compare-dialog" aria-label="Compare products">
  <div id="searchbox"></div>
  <div id="hits"></div>
  <footer id="compare-actions"></footer>
</dialog>
<div id="chat"></div>
*/

const dialog = document.querySelector('#compare-dialog');

document.querySelector('#open-compare').addEventListener('click', () => {
  dialog.showModal();
});

const search = instantsearch({ indexName: 'instant_search', searchClient });

const compareActions = connectCompare((renderOptions, isFirstRender) => {
  const { items, canCompare, maxItems, compare, clearItems } = renderOptions;
  const container = document.querySelector('#compare-actions');

  if (isFirstRender) {
    container.addEventListener('click', (event) => {
      if (event.target.matches('[data-compare]')) {
        // `compare()` sends the request and clears the selection.
        // It returns `false` when the chat is busy or the selection is too small.
        if (compare()) {
          dialog.close();
        }
      }
      if (event.target.matches('[data-cancel]')) {
        clearItems();
        dialog.close();
      }
    });
  }

  container.innerHTML = `
    <span>${items.length} of ${maxItems} selected</span>
    <button type="button" data-cancel>Cancel</button>
    <button type="button" data-compare ${canCompare ? '' : 'disabled'}>
      Compare ${items.length}
    </button>
  `;
});

search.addWidgets([
  searchBox({ container: '#searchbox' }),
  hits({
    container: '#hits',
    templates: {
      item(hit, { html }) {
        const { isSelected, toggleItem, canAddItems } =
          search.renderState.instant_search?.compare ?? {};
        const selected = isSelected?.(hit.objectID) ?? false;

        return html`
          <article>
            <h2>${hit.name}</h2>
            <label>
              <input
                type="checkbox"
                checked=${selected}
                disabled=${!toggleItem || (!selected && !canAddItems)}
                onChange=${() => toggleItem?.(hit)}
              />
              Compare
            </label>
          </article>
        `;
      },
    },
  }),
  compareActions({
    minItems: 2,
    maxItems: 3,
    configurationId: 'CONFIGURATION_ID',
  }),
  chat({ container: '#chat', agentId: 'AGENT_ID' }),
]);

search.start();
```

On a page that already has search results, add the **Compare** toggle to those results instead, as shown in the [integration step](#enable-and-integrate-a-configuration):
a search box inside the dialog would change the page's query.

## Render a comparison table from product records

The chat widget includes a renderer for the `algolia_compare_products` client-side tool.
The table has one column per selected product and one row per comparison criterion.
When you enable a comparison configuration, Agent Studio adds this tool to your agent, and the default instructions tell the agent to use it.
While a configuration is enabled, the tool's name and input schema are read-only in the agent's **Tools** list.
Without the tool, the agent writes the comparison as a Markdown table in its answer.

The agent passes the product object IDs and the criteria to `algolia_compare_products`, along with optional introductory text.
Each criterion has a label and one value per product, taken from the product records, including details from the descriptions.
The chat widget renders each product header with the same item template as the search results, using the records it holds: the selected products and any search results from the conversation.
Missing values appear as `—`.

While a configuration is enabled, you can edit the tool's description from the agent's **Tools** list, the way you edit the instructions.
The model reads the description to decide when to call the tool and what to pass, so use it for guidance that applies to every comparison, such as "Lead with price".
The tool name and input schema are fixed, because the chat widget renders the table from them.
Select **Reset to default** to return to the default description.

If you manage the agent through the API instead, add the tool to the agent's `tools` as a [client-side tool](https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/tools/client-side-tools.md), in the shape the [Agent Studio API](https://www.algolia.com/doc/rest-api/agent-studio.md) expects:

```json JSON icon=braces expandable theme={"system"}
{
  "type": "client_side",
  "name": "algolia_compare_products",
  "description": "Renders a comparison table for the user: selected products across the top, one row per criterion. Pass objectIDs and criteria with one value per product from the records. Never repeat it in text.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "objectIDs": {
        "type": "array",
        "description": "The objectIDs of the products to compare, one column each, in display order",
        "items": { "type": "string" }
      },
      "criteria": {
        "type": "array",
        "description": "The comparison rows. Each row has a short label and one value per product, in the same order as objectIDs. Take values from the product records, including details from the description; use null when a record has no value.",
        "items": {
          "type": "object",
          "properties": {
            "label": { "type": "string", "description": "Short row label, e.g. \"Battery life\"" },
            "values": {
              "type": "array",
              "description": "One value per product, aligned to objectIDs; null when not listed",
              "items": { "type": ["string", "null"] }
            }
          },
          "required": ["label", "values"],
          "additionalProperties": false
        }
      },
      "intro": {
        "type": ["string", "null"],
        "description": "Optional one-line introduction shown above the table"
      }
    },
    "required": ["objectIDs", "criteria", "intro"]
  }
}
```

The widget shows one column per object ID, in the order the agent passes them, and one row per criterion.
A value that's missing, `null`, or shorter than the list of products appears as `—`.

To change how the table looks, override the comparison tool in the chat widget's `tools` object,
using `CompareProductsToolType` as the property name.
For more information,
see the chat widget reference for [React](https://www.algolia.com/doc/api-reference/widgets/chat/react.md#param-tools) or [JavaScript](https://www.algolia.com/doc/api-reference/widgets/chat/js.md#param-tools).

## Send a compare request without InstantSearch

If you don't use InstantSearch, send the compare request to the completions endpoint yourself.
The user message text is a placeholder that names the configuration, and the selection travels as per-turn context:

```sh Command line icon=square-terminal theme={"system"}
curl "https://$ALGOLIA_APPLICATION_ID.algolia.net/agent-studio/1/agents/$AGENT_ID/completions?compatibilityMode=ai-sdk-5" \
  -X POST \
  -H 'Content-Type: application/json' \
  -H "x-algolia-application-id: $ALGOLIA_APPLICATION_ID" \
  -H "X-Algolia-API-Key: $ALGOLIA_SEARCH_API_KEY" \
  --data-raw '{
  "messages": [
    {
      "role": "user",
      "parts": [{ "type": "text", "text": "__ALGOLIA_COMPARISON_CONFIGURATION_ID__" }],
      "metadata": {
        "turnContext": {
          "comparison_configuration_id": "CONFIGURATION_ID",
          "selected_products": "[{\"objectID\":\"1\",\"name\":\"Product A\"},{\"objectID\":\"2\",\"name\":\"Product B\"}]"
        }
      }
    }
  ]
}'
```

* Replace `CONFIGURATION_ID` with the configuration ID, in both the placeholder and the turn context.
  The match is case-sensitive.
* The placeholder must be the entire text of the user message.
  A message that wraps it in other text passes through to the agent unchanged.
* `selected_products` is the selected records serialized as a JSON string.
  Remove internal attributes such as `_highlightResult` before serializing them.

Agent Studio replaces the placeholder with the configured instructions for the model and stores the user message without text.
The response stream includes a transient `data-prompt-shortcut` part with the placeholder as `sentinel` and an empty `displayText`:
don't show a user message for the request, as the InstantSearch chat widget does.
If the configuration doesn't exist or you turned it off, the message passes through unchanged, so only send the placeholder for an enabled configuration.
For the request schema, see the [Agent Studio API reference](https://www.algolia.com/doc/rest-api/agent-studio.md).

## Manage configurations

From the configurations list, you can:

* Search configurations by name.
* Duplicate a configuration to use it as a starting point. The copy is turned off by default.
* Delete a configuration. Disable it first: you can't delete an enabled configuration.

You can create up to 15 configurations per agent.
The **Create** button shows the limit when you reach it.
