Skip to content

Commit 13661c7

Browse files
dmitrycnstrcdmitrycnstrc
andauthored
[AT-68]: agent overview update javascript client repo (#432)
* add agent-overview module * update readme * fix linter * fixes after copilot review * fix copilot review * return condition for strings * remove agent-overview module & update agent module * remove agent-overview module * return event types * return even types comment * fix types * fix conditions for agent & description * fix types * update description * update agent test * update doc URLs * fix typo --------- Co-authored-by: dmitrycnstrc <dmitrii.gulevskii@constructor.io>
1 parent 4678f25 commit 13661c7

4 files changed

Lines changed: 246 additions & 6 deletions

File tree

spec/src/modules/agent.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,141 @@ describe(`ConstructorIO - Agent${bundledDescriptionSuffix}`, () => {
9393
expect(url).not.contain(' ');
9494
expect(url).contain(encodeURIComponentRFC3986(intentWithSpaces));
9595
});
96+
97+
it('should include threadId parameter when provided', () => {
98+
const url = createAgentUrl(
99+
'running shoes',
100+
{ ...defaultParameters, threadId: 'test-thread-id' },
101+
defaultOptions,
102+
);
103+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
104+
105+
expect(requestedUrlParams).to.have.property('thread_id').to.equal('test-thread-id');
106+
});
107+
108+
it('should include guard parameter when provided as true', () => {
109+
const url = createAgentUrl(
110+
'running shoes',
111+
{ ...defaultParameters, guard: true },
112+
defaultOptions,
113+
);
114+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
115+
116+
expect(requestedUrlParams).to.have.property('guard').to.equal('true');
117+
});
118+
119+
it('should include guard parameter when provided as false', () => {
120+
const url = createAgentUrl(
121+
'running shoes',
122+
{ ...defaultParameters, guard: false },
123+
defaultOptions,
124+
);
125+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
126+
127+
expect(requestedUrlParams).to.have.property('guard').to.equal('false');
128+
});
129+
130+
it('should include numResultsPerEvent parameter when provided', () => {
131+
const url = createAgentUrl(
132+
'running shoes',
133+
{ ...defaultParameters, numResultsPerEvent: 5 },
134+
defaultOptions,
135+
);
136+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
137+
138+
expect(requestedUrlParams).to.have.property('num_results_per_event').to.equal('5');
139+
});
140+
141+
it('should include numResultEvents parameter when provided', () => {
142+
const url = createAgentUrl(
143+
'running shoes',
144+
{ ...defaultParameters, numResultEvents: 3 },
145+
defaultOptions,
146+
);
147+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
148+
149+
expect(requestedUrlParams).to.have.property('num_result_events').to.equal('3');
150+
});
151+
152+
it('should include numResultsPerPage parameter when provided', () => {
153+
const url = createAgentUrl(
154+
'running shoes',
155+
{ ...defaultParameters, numResultsPerPage: 10 },
156+
defaultOptions,
157+
);
158+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
159+
160+
expect(requestedUrlParams).to.have.property('num_results_per_page').to.equal('10');
161+
});
162+
163+
it('should include preFilterExpression as JSON string when provided as object', () => {
164+
const preFilterExpression = { and: [{ name: 'brand', value: 'Nike' }] };
165+
const url = createAgentUrl(
166+
'running shoes',
167+
{ ...defaultParameters, preFilterExpression },
168+
defaultOptions,
169+
);
170+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
171+
172+
expect(requestedUrlParams).to.have.property('pre_filter_expression').to.equal(JSON.stringify(preFilterExpression));
173+
});
174+
175+
it('should include qsParam as JSON string when provided as object', () => {
176+
const qsParam = { section: 'Products' };
177+
const url = createAgentUrl(
178+
'running shoes',
179+
{ ...defaultParameters, qsParam },
180+
defaultOptions,
181+
);
182+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
183+
184+
expect(requestedUrlParams).to.have.property('qs').to.equal(JSON.stringify(qsParam));
185+
});
186+
187+
it('should include fmtOptions when provided', () => {
188+
const fmtOptions = { fields: ['title', 'image_url'], hidden_fields: ['price'] };
189+
const url = createAgentUrl(
190+
'running shoes',
191+
{ ...defaultParameters, fmtOptions },
192+
defaultOptions,
193+
);
194+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
195+
196+
expect(requestedUrlParams).to.have.property('fmt_options');
197+
expect(requestedUrlParams.fmt_options).to.have.property('fields');
198+
expect(requestedUrlParams.fmt_options).to.have.property('hidden_fields');
199+
});
200+
201+
it('should include user segments when provided in options', () => {
202+
const url = createAgentUrl('running shoes', defaultParameters, {
203+
...defaultOptions,
204+
segments: ['segment1', 'segment2'],
205+
});
206+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
207+
208+
expect(requestedUrlParams).to.have.property('us');
209+
});
210+
211+
it('should include userId when provided in options', () => {
212+
const url = createAgentUrl('running shoes', defaultParameters, {
213+
...defaultOptions,
214+
userId: 'user-123',
215+
});
216+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
217+
218+
expect(requestedUrlParams).to.have.property('ui').to.equal('user-123');
219+
});
220+
221+
it('should include testCells when provided in options', () => {
222+
const url = createAgentUrl('running shoes', defaultParameters, {
223+
...defaultOptions,
224+
testCells: { cellA: 'variant1', cellB: 'variant2' },
225+
});
226+
const requestedUrlParams = qs.parse(url.split('?')?.[1]);
227+
228+
expect(requestedUrlParams).to.have.property('ef-cellA').to.equal('variant1');
229+
expect(requestedUrlParams).to.have.property('ef-cellB').to.equal('variant2');
230+
});
96231
});
97232

98233
// setupEventListeners util Tests
@@ -196,6 +331,42 @@ describe(`ConstructorIO - Agent${bundledDescriptionSuffix}`, () => {
196331
done();
197332
});
198333
});
334+
335+
it('should enqueue MESSAGE event data into the stream', (done) => {
336+
const eventType = Agent.EventTypes.MESSAGE;
337+
const eventData = { text: 'Here are some recommendations' };
338+
339+
setupEventListeners(mockEventSource, mockStreamController, Agent.EventTypes);
340+
341+
const messageCallback = mockEventSource.addEventListener
342+
.getCalls()
343+
.find((call) => call.args[0] === eventType).args[1];
344+
345+
messageCallback({ data: JSON.stringify(eventData) });
346+
347+
setImmediate(() => {
348+
expect(mockStreamController.enqueue.calledWith({ type: eventType, data: eventData })).to.be.true;
349+
done();
350+
});
351+
});
352+
353+
it('should enqueue FOLLOW_UP_QUESTIONS event data into the stream', (done) => {
354+
const eventType = Agent.EventTypes.FOLLOW_UP_QUESTIONS;
355+
const eventData = { questions: ['What size?', 'What color?'] };
356+
357+
setupEventListeners(mockEventSource, mockStreamController, Agent.EventTypes);
358+
359+
const followUpCallback = mockEventSource.addEventListener
360+
.getCalls()
361+
.find((call) => call.args[0] === eventType).args[1];
362+
363+
followUpCallback({ data: JSON.stringify(eventData) });
364+
365+
setImmediate(() => {
366+
expect(mockStreamController.enqueue.calledWith({ type: eventType, data: eventData })).to.be.true;
367+
done();
368+
});
369+
});
199370
});
200371

201372
describe('getAgentResultsStream', () => {
@@ -265,4 +436,5 @@ describe(`ConstructorIO - Agent${bundledDescriptionSuffix}`, () => {
265436
reader.cancel();
266437
});
267438
});
439+
268440
});

src/modules/agent.js

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
const { cleanParams, trimNonBreakingSpaces, encodeURIComponentRFC3986, stringify } = require('../utils/helpers');
1+
const { cleanParams, trimNonBreakingSpaces, encodeURIComponentRFC3986, stringify, isNil } = require('../utils/helpers');
22

33
// Create URL from supplied intent (term) and parameters
4+
// eslint-disable-next-line complexity
45
function createAgentUrl(intent, parameters, options) {
56
const {
67
apiKey,
@@ -26,7 +27,7 @@ function createAgentUrl(intent, parameters, options) {
2627
}
2728

2829
// Validate domain is provided
29-
if (!parameters.domain || typeof parameters.domain !== 'string') {
30+
if (!parameters || !parameters.domain || typeof parameters.domain !== 'string') {
3031
throw new Error('parameters.domain is a required parameter of type string');
3132
}
3233

@@ -48,7 +49,17 @@ function createAgentUrl(intent, parameters, options) {
4849
}
4950

5051
if (parameters) {
51-
const { domain, numResultsPerPage } = parameters;
52+
const {
53+
domain,
54+
numResultsPerPage,
55+
threadId,
56+
guard,
57+
numResultsPerEvent,
58+
numResultEvents,
59+
qsParam,
60+
preFilterExpression,
61+
fmtOptions,
62+
} = parameters;
5263

5364
// Pull domain from parameters
5465
if (domain) {
@@ -59,6 +70,41 @@ function createAgentUrl(intent, parameters, options) {
5970
if (numResultsPerPage) {
6071
queryParams.num_results_per_page = numResultsPerPage;
6172
}
73+
74+
// Pull thread_id from parameters
75+
if (threadId) {
76+
queryParams.thread_id = threadId;
77+
}
78+
79+
// Pull guard from parameters
80+
if (!isNil(guard)) {
81+
queryParams.guard = guard;
82+
}
83+
84+
// Pull num_results_per_event from parameters
85+
if (numResultsPerEvent) {
86+
queryParams.num_results_per_event = numResultsPerEvent;
87+
}
88+
89+
// Pull num_result_events from parameters
90+
if (numResultEvents) {
91+
queryParams.num_result_events = numResultEvents;
92+
}
93+
94+
// Pull qsParam from parameters
95+
if (qsParam) {
96+
queryParams.qs = JSON.stringify(qsParam);
97+
}
98+
99+
// Pull pre_filter_expression from parameters
100+
if (preFilterExpression) {
101+
queryParams.pre_filter_expression = JSON.stringify(preFilterExpression);
102+
}
103+
104+
// Pull fmt_options from parameters
105+
if (fmtOptions) {
106+
queryParams.fmt_options = fmtOptions;
107+
}
62108
}
63109

64110
// eslint-disable-next-line no-underscore-dangle
@@ -124,6 +170,8 @@ class Agent {
124170
RECIPE_INSTRUCTIONS: 'recipe_instructions', // Represents recipe instructions
125171
SERVER_ERROR: 'server_error', // Server Error event
126172
IMAGE_META: 'image_meta', // This event type is used for enhancing recommendations with media content such as images
173+
MESSAGE: 'message', // Represents a textual message from the agent
174+
FOLLOW_UP_QUESTIONS: 'follow_up_questions', // Represents follow-up question suggestions
127175
END: 'end', // Represents the end of data stream
128176
};
129177

@@ -133,9 +181,18 @@ class Agent {
133181
* @function getAgentResultsStream
134182
* @description Retrieve a stream of agent results from Constructor.io API
135183
* @param {string} intent - Intent to use to perform an intent based recommendations
136-
* @param {object} [parameters] - Additional parameters to refine result set
137-
* @param {string} [parameters.domain] - domain name e.g. swimming sports gear, groceries
138-
* @param {number} [parameters.numResultsPerPage] - The total number of results to return
184+
* @param {object} parameters - Additional parameters to refine result set
185+
* @param {string} parameters.domain - Domain name (e.g. "groceries", "recipes")
186+
* @param {string} [parameters.threadId] - Conversation thread ID for multi-turn dialogue
187+
* @param {boolean} [parameters.guard] - Enable content moderation
188+
* @param {number} [parameters.numResultsPerEvent] - Max products per search_result event
189+
* @param {number} [parameters.numResultEvents] - Max number of search_result events
190+
* @param {number} [parameters.numResultsPerPage] - Deprecated: use numResultsPerEvent instead
191+
* @param {object} [parameters.preFilterExpression] - Faceting expression to scope search results. Please refer to https://docs.constructor.com/reference/configuration-collections
192+
* @param {object} [parameters.fmtOptions] - The format options used to refine result groups. Please refer to https://docs.constructor.com/reference/v1-asa-retrieve-intent#query-params for details
193+
* @param {string[]} [parameters.fmtOptions.fields] - Product fields to return
194+
* @param {string[]} [parameters.fmtOptions.hidden_fields] - Hidden fields to return
195+
* @param {object} [parameters.qsParam] - Parameters listed above can be serialized into a JSON object and parsed through this parameter. Please refer to https://docs.constructor.com/reference/v1-asa-retrieve-intent#query-params
139196
* @returns {ReadableStream} Returns a ReadableStream.
140197
* @example
141198
* const readableStream = constructorio.agent.getAgentResultsStream('I want to get shoes', {

src/types/agent.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import {
22
ConstructorClientOptions,
3+
FmtOptions,
4+
FilterExpression,
35
} from '.';
46

57
export default Agent;
68

79
export interface IAgentParameters {
810
domain: string;
11+
/** @deprecated Use numResultsPerEvent instead */
912
numResultsPerPage?: number;
1013
filters?: Record<string, any>;
14+
threadId?: string;
15+
guard?: boolean;
16+
numResultsPerEvent?: number;
17+
numResultEvents?: number;
18+
qsParam?: Record<string, any>;
19+
preFilterExpression?: FilterExpression;
20+
fmtOptions?: Pick<FmtOptions, 'fields' | 'hidden_fields'>;
1121
}
1222

1323
declare class Agent {

src/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default ConstructorIO;
66
export * from './search';
77
export * from './autocomplete';
88
export * from './quizzes';
9+
export * from './agent';
910
export * from './recommendations';
1011
export * from './browse';
1112
export * from './tracker';

0 commit comments

Comments
 (0)