modules/pia.js

/* eslint-disable object-curly-newline, no-underscore-dangle */
const EventDispatcher = require('../utils/event-dispatcher');
const helpers = require('../utils/helpers');

// Create URL from supplied itemId and parameters
function createPiaUrl(itemId, parameters, options, questionPath) {
  const {
    apiKey,
    clientId,
    sessionId,
    segments,
    userId,
    version,
    agentServiceUrl,
    assistantServiceUrl,
  } = options;
  const serviceUrl = agentServiceUrl || assistantServiceUrl;
  let queryParams = { c: version };

  queryParams.key = apiKey;
  queryParams.i = clientId;
  queryParams.s = sessionId;

  // Validate item id is provided
  if (!itemId || typeof itemId !== 'string') {
    throw new Error('itemId is a required parameter of type string');
  }

  queryParams.item_id = itemId;

  // Pull user segments from options
  if (segments && segments.length) {
    queryParams.us = segments;
  }

  // Pull user id from options and ensure string
  if (userId) {
    queryParams.ui = String(userId);
  }

  if (parameters) {
    const { threadId, variationId, numResults } = parameters;

    if (threadId) {
      queryParams.thread_id = threadId;
    }

    if (variationId) {
      queryParams.variation_id = variationId;
    }

    if (!helpers.isNil(numResults)) {
      queryParams.num_results = numResults;
    }
  }

  queryParams._dt = Date.now();
  queryParams = helpers.cleanParams(queryParams);

  const queryString = helpers.stringify(queryParams);

  return `${serviceUrl}/v1/item_questions${questionPath}?${queryString}`;
}

/**
 * Interface to Product Insights Agent (PIA) related API calls
 *
 * @module pia
 * @inner
 * @returns {object}
 */
class Pia {
  constructor(options) {
    this.options = options || {};
    this.eventDispatcher = new EventDispatcher(options.eventDispatcher);
  }

  /**
   * Retrieve suggested questions for an item
   *
   * @function getSuggestedQuestions
   * @description Retrieve suggested questions for a product from Constructor.io API
   * @param {string} itemId - The identifier of the item
   * @param {object} [parameters] - Additional parameters to refine result set
   * @param {string} [parameters.threadId] - Thread ID for conversation context (UUID)
   * @param {string} [parameters.variationId] - Variation ID of the item
   * @param {number} [parameters.numResults] - Number of suggested questions to return
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {Promise}
   * @example
   * constructorio.agent.pia.getSuggestedQuestions('item-123', {
   *    variationId: 'variation-456',
   *    numResults: 3,
   * });
   */
  getSuggestedQuestions(itemId, parameters, networkParameters = {}) {
    let requestUrl;
    const { fetch } = this.options;
    let signal;

    if (typeof AbortController === 'function') {
      const controller = new AbortController();

      signal = controller && controller.signal;

      helpers.applyNetworkTimeout(this.options, networkParameters, controller);
    }

    try {
      requestUrl = createPiaUrl(itemId, parameters, this.options, '');
    } catch (e) {
      return Promise.reject(e);
    }

    return fetch(requestUrl, { signal })
      .then(helpers.convertResponseToJson)
      .then((json) => {
        if (json.questions) {
          this.eventDispatcher.queue('pia.getSuggestedQuestions.completed', json);

          return json;
        }

        throw new Error('getSuggestedQuestions response data is malformed');
      });
  }

  /**
   * Retrieve an answer for a question about an item
   *
   * @function getAnswerResults
   * @description Retrieve an answer to a product question from Constructor.io API
   * @param {string} itemId - The identifier of the item
   * @param {string} question - The question to ask about the item
   * @param {object} [parameters] - Additional parameters to refine result set
   * @param {string} [parameters.threadId] - Thread ID for conversation context (UUID)
   * @param {string} [parameters.variationId] - Variation ID of the item
   * @param {object} [networkParameters] - Parameters relevant to the network request
   * @param {number} [networkParameters.timeout] - Request timeout (in milliseconds)
   * @returns {Promise}
   * @example
   * constructorio.agent.pia.getAnswerResults('item-123', 'What material is this made of?', {
   *    threadId: '550e8400-e29b-41d4-a716-446655440000',
   * });
   */
  getAnswerResults(itemId, question, parameters, networkParameters = {}) {
    let requestUrl;
    const { fetch } = this.options;
    let signal;

    if (!question || typeof question !== 'string') {
      return Promise.reject(new Error('question is a required parameter of type string'));
    }

    if (typeof AbortController === 'function') {
      const controller = new AbortController();

      signal = controller && controller.signal;

      helpers.applyNetworkTimeout(this.options, networkParameters, controller);
    }

    try {
      const encodedQuestion = helpers.encodeURIComponentRFC3986(helpers.trimNonBreakingSpaces(question));
      requestUrl = createPiaUrl(itemId, parameters, this.options, `/${encodedQuestion}/answer`);
    } catch (e) {
      return Promise.reject(e);
    }

    return fetch(requestUrl, { signal })
      .then(helpers.convertResponseToJson)
      .then((json) => {
        if (json.qna_result_id) {
          this.eventDispatcher.queue('pia.getAnswerResults.completed', json);

          return json;
        }

        throw new Error('getAnswerResults response data is malformed');
      });
  }
}

module.exports = Pia;