22
33import {
44 createGroundedComparisonTableComponent ,
5+ criteriaFromAttributes ,
56 defaultComparisonTableTranslations ,
67} from './createGroundedComparisonTable' ;
78
8- import type { Renderer } from '../../../types' ;
9+ import type { RecordWithObjectID , Renderer } from '../../../types' ;
910import type { ClientSideToolComponentProps } from '../types' ;
10- import type { ComparisonTableTranslations } from './createGroundedComparisonTable' ;
11+ import type {
12+ ComparisonCriterion ,
13+ ComparisonTableTranslations ,
14+ GroundedComparisonTableProps ,
15+ } from './createGroundedComparisonTable' ;
1116
1217/**
1318 * Builtin comparison tool (`algolia_compare_products`).
1419 *
15- * The agent triggers this client-side tool when it detects a comparison
16- * request. Its arguments name ONLY the products (by objectID) and the attribute
17- * *keys* to compare — never the values. Every cell is hydrated from the chat
18- * records store (`context.records`, filled by the shopper's comparison
19- * selection and by the search tools), so the model physically cannot type
20- * (and therefore cannot hallucinate) a price or spec.
21- * This is the "grounded table" fix from the agentic-evals comparison study,
22- * promoted from the display-block prototype to a first-class tool.
20+ * The agent calls this client-side tool to lay out a comparison as a table:
21+ * the compared products across the top (hydrated from the chat records store,
22+ * `context.records`, so the header never shows a product that isn't in the
23+ * catalog) and model-authored criteria down the side. Each criterion carries
24+ * one value per product; the agent fills them from the records it has (an
25+ * attribute, a detail from the description, a derived verdict), so the table
26+ * can say everything a Markdown table could — with a proper layout.
2327 *
2428 * Tool-call input it consumes:
2529 *
2630 * {
27- * objectIDs: ['A', 'B'], // 2+ products, one row each
28- * attributes: ['price', 'rating'], // attribute KEYS (also headers)
29- * columns?: ['Product', 'Price', 'Rating'], // optional display labels
30- * intro?: 'Both are great for...' // optional one-line lead-in
31+ * objectIDs: ['A', 'B'], // 2+ products, one column each
32+ * criteria: [ // one row each, values aligned
33+ * { label: 'Price', values: ['$199', '$299'] },
34+ * { label: 'Battery', values: ['4000 mAh', null] }, // null → "—"
35+ * ],
36+ * intro?: 'Both are great for...' // optional one-line lead-in
3137 * }
3238 *
33- * Any other input field (e.g. smuggled attribute values) is ignored by
34- * construction: the renderer never reads values off the input.
39+ * The attribute-based shape (`attributes: ['price']`, values read off the
40+ * records) is still accepted for agents configured with the previous tool
41+ * definition.
3542 */
3643
3744export type CompareProductsToolInput = {
3845 objectIDs ?: unknown ;
46+ criteria ?: unknown ;
3947 attributes ?: unknown ;
4048 columns ?: unknown ;
4149 intro ?: unknown ;
4250} ;
4351
44- export type CompareProductsToolProps = {
52+ export type CompareProductsToolProps <
53+ THit extends RecordWithObjectID = RecordWithObjectID ,
54+ > = {
4555 toolProps : ClientSideToolComponentProps ;
56+ itemComponent ?: GroundedComparisonTableProps < THit > [ 'itemComponent' ] ;
4657 translations ?: Partial < ComparisonTableTranslations > ;
4758} ;
4859
@@ -56,18 +67,68 @@ function stringList(value: unknown): string[] {
5667 ) ;
5768}
5869
59- export function createCompareProductsToolComponent ( {
60- createElement,
61- Fragment,
62- } : Renderer ) {
63- const GroundedComparisonTable = createGroundedComparisonTableComponent ( {
70+ /** Keeps only well-formed `{ label, values[] }` rows. */
71+ function criteriaList ( value : unknown ) : ComparisonCriterion [ ] {
72+ if ( ! Array . isArray ( value ) ) {
73+ return [ ] ;
74+ }
75+ return value . flatMap ( ( entry ) : ComparisonCriterion [ ] => {
76+ if (
77+ ! entry ||
78+ typeof entry !== 'object' ||
79+ typeof ( entry as ComparisonCriterion ) . label !== 'string' ||
80+ ( entry as ComparisonCriterion ) . label === ''
81+ ) {
82+ return [ ] ;
83+ }
84+ const values = ( entry as ComparisonCriterion ) . values ;
85+ return [
86+ {
87+ label : ( entry as ComparisonCriterion ) . label ,
88+ values : Array . isArray ( values ) ? values : [ ] ,
89+ } ,
90+ ] ;
91+ } ) ;
92+ }
93+
94+ /**
95+ * Previous tool contract: `attributes` (keys read off the records) plus an
96+ * optional `columns` list labelling `[product, ...attributes]`.
97+ */
98+ function legacyCriteria (
99+ objectIDs : string [ ] ,
100+ input : CompareProductsToolInput ,
101+ records : ClientSideToolComponentProps [ 'context' ] [ 'records' ]
102+ ) : ComparisonCriterion [ ] {
103+ const attributes = stringList ( input . attributes ) ;
104+ const columns = Array . isArray ( input . columns )
105+ ? stringList ( input . columns )
106+ : undefined ;
107+ const labels =
108+ columns && columns . length === attributes . length + 1
109+ ? columns . slice ( 1 )
110+ : columns ;
111+
112+ return criteriaFromAttributes ( objectIDs , attributes , records , labels ) ;
113+ }
114+
115+ export function createCompareProductsToolComponent <
116+ THit extends RecordWithObjectID = RecordWithObjectID ,
117+ > ( { createElement, Fragment } : Renderer ) {
118+ const GroundedComparisonTable = createGroundedComparisonTableComponent < THit > ( {
64119 createElement,
65120 Fragment,
66121 } ) ;
67122
68- return function CompareProductsTool ( userProps : CompareProductsToolProps ) {
69- const { toolProps, translations : userTranslations } = userProps ;
70- const { message, records } = toolProps . context ;
123+ return function CompareProductsTool (
124+ userProps : CompareProductsToolProps < THit >
125+ ) {
126+ const {
127+ toolProps,
128+ itemComponent,
129+ translations : userTranslations ,
130+ } = userProps ;
131+ const { message, records, sendEvent } = toolProps . context ;
71132
72133 const translations : ComparisonTableTranslations = {
73134 ...defaultComparisonTableTranslations ,
@@ -85,23 +146,25 @@ export function createCompareProductsToolComponent({
85146
86147 const input = ( message . input ?? { } ) as CompareProductsToolInput ;
87148 const objectIDs = stringList ( input . objectIDs ) ;
88- const attributes = stringList ( input . attributes ) ;
89- const columns = Array . isArray ( input . columns )
90- ? stringList ( input . columns )
91- : undefined ;
92149 const intro = typeof input . intro === 'string' ? input . intro : undefined ;
93150
94151 if ( objectIDs . length === 0 ) {
95152 return < Fragment /> ;
96153 }
97154
155+ const criteria =
156+ input . criteria !== undefined
157+ ? criteriaList ( input . criteria )
158+ : legacyCriteria ( objectIDs , input , records ) ;
159+
98160 return (
99161 < GroundedComparisonTable
100162 intro = { intro }
101163 objectIDs = { objectIDs }
102- attributes = { attributes }
103- columns = { columns }
164+ criteria = { criteria }
104165 records = { records }
166+ itemComponent = { itemComponent }
167+ sendEvent = { sendEvent }
105168 translations = { translations }
106169 />
107170 ) ;
0 commit comments