ext/ai-help-bot/lib/codio-ide-api.js

import _ from 'lodash'

import events from '../../../core/events'
import { MESSAGE_OWNER } from './constants'

const EVENTS = {
  open: 'aiHelpBot:open',
  close: 'aiHelpBot:close',
  call: 'aiHelpBot:call',
  showTooltip: 'aiHelpBot:showTooltip',
  hideTooltip: 'aiHelpBot:hideTooltip',
  register: 'aiHelpBot:register',
  deregister: 'aiHelpBot:deregister',
  showButton: 'aiHelpBot:showButton',
  write: 'aiHelpBot:write',
  input: 'aiHelpBot:input',
  getContext: 'aiHelpBot:getContext',
  ask: 'aiHelpBot:ask',
  getHistory: 'aiHelpBot:getHistory',
  showMenu: 'aiHelpBot:showMenu',
  getLlmProxyDetails: 'aiHelpBot:getLlmProxyDetails'
}

const emit = events.create(EVENTS)

/**
 * Codio IDE API object
 * @exports window.codioIDE
 * @type {object}
 * @namespace codioIDE
 * @global
 */
window.codioIDE = window.codioIDE || {}

/**
 * Coach Bot API methods
 * @memberof codioIDE
 * @type {object}
 * @namespace codioIDE.coachBot
 */
window.codioIDE.coachBot = {
  /**
   * Simple callback
   * @callback codioIDE.coachBot.Callback
   * @memberof codioIDE.coachBot
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotCallOptions
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {string} id - action id to start
   * @property {Object} [defaultValues] - default values for action
   * @property {Object} [params] - additional params for action that will be passed to the action handler
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotContext.GuidesPage
   * @memberof codioIDE.coachBot.CoachBotContext
   * @type {Object}
   * @property {string} page - current guides page name
   * @property {string} content - current guides page content as text
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotContext.AssignmentData
   * @memberof codioIDE.coachBot.CoachBotContext
   * @type {Object}
   * @property {string} courseName - course name
   * @property {string} assignmentName - assignment name
   * @property {string} userId - user id
   * @property {string} userName - username
   * @property {string} userLogin - user login
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotContext.FileInfo
   * @memberof codioIDE.coachBot.CoachBotContext
   * @type {Object}
   * @property {string} path -  file path
   * @property {string} content - file content
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotContext.ErrorInfo
   * @memberof codioIDE.coachBot.CoachBotContext
   * @type {Object}
   * @property {boolean} errorState - has error or not
   * @property {string} text - error text
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotContext
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {codioIDE.coachBot.CoachBotContext.GuidesPage} guidesPage - opened guides page info
   * @property {codioIDE.coachBot.CoachBotContext.AssignmentData} assignmentData - assignment info
   * @property {codioIDE.coachBot.CoachBotContext.FileInfo[]} files - opened files info
   * @property {codioIDE.coachBot.CoachBotContext.ErrorInfo} error - current error info
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotMessage
   * @type {Object}
   * @property {codioIDE.coachBot.MESSAGE_ROLES} role - message role
   * @property {string} content - message text
   */

  /**
   * @typedef codioIDE.coachBot.LlmProxyDetails
   * @type {Object}
   * @property {string} provider - proxy provider name
   * @property {string} endpoint - proxy endpoint
   * @property {string} key - proxy key
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotCustomRequestData
   * @description Custom request data. If userPrompt was specified it will be sent as user data otherwise messages will be sent.
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {string} systemPrompt - system prompt
   * @property {string} userPrompt - user prompt
   * @property {codioIDE.coachBot.CoachBotMessage} [messages] - input messages
   * - roles must alternate between USER and ASSISTANT
   * - first message must use the USER role
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotCustomProxyOptions
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {string} provider - proxy LLM provider
   * @property {string} endpoint - optional LLM proxy https endpoint
   * @property {string} key - optional LLM proxy access key
   * @property {string} model - optional LLM proxy AI model
   * @property {number} temperature - optional LLM proxy AI temperature. Range depend on a provider and a model.
   * @property {number} maxTokens - optional LLM proxy maxTokens value. Range is 1-1024
   * @property {string} apiVersion - optional LLM proxy AI model api version
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotCustomRequestOptions
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {boolean} [stream=true] - stream response to coach bot
   * @property {boolean} [preventMenu=false] - allow user to prevent menu
   * @property {codioIDE.coachBot.CoachBotCustomProxyOptions} proxy - optional custom ask proxy configuration
   */

  /**
   * @typedef codioIDE.coachBot.CoachBotAskCallbackData
   * @memberof codioIDE.coachBot
   * @type {Object}
   * @property {boolean} interrupted - was interrupted by user(user click 'back' button)
   * @property {Object} error - request error
   */

  /**
   * Ask bot done callback
   * @memberof codioIDE.coachBot
   * @callback codioIDE.coachBot.AskBotDoneCallback
   * @param {codioIDE.coachBot.CoachBotAskCallbackData} result - done result
   */

  /**
   * Opens Coach Bot
   * @memberof codioIDE.coachBot
   * @method open
   * @param {codioIDE.coachBot.CoachBotCallOptions} [options] - optional call action options
   */
  open: (options) => {
    emit.open(options)
  },
  /**
   * Closes Coach Bot
   * @memberof codioIDE.coachBot
   * @method close
   */
  close: () => {
    emit.close()
  },
  /**
   * Opens Coach Bot and calls action
   * @memberof codioIDE.coachBot
   * @method call
   * @param {codioIDE.coachBot.CoachBotCallOptions} [options] - optional call action options
   */
  call: (options) => {
    emit.call(options)
  },
  /**
   * Show Coach bot tooltip
   * @memberof codioIDE.coachBot
   * @method showTooltip
   * @param {string} message - tooltip message
   * @param {codioIDE.coachBot.Callback} callback - executes when tooltip clicked
   */
  showTooltip: (message, callback) => {
    emit.showTooltip({
      message,
      callback: () => {
        callback && callback()
      }
    })
  },
  /**
   * Hide Coach bot tooltip
   * @memberof codioIDE.coachBot
   * @method hideTooltip
   */
  hideTooltip: () => {
    emit.hideTooltip()
  },
  /**
   * Register coach bot menu item
   * @memberof codioIDE.coachBot
   * @method register
   * @property {string} id - A unique ID.
   * @property {string} name - Button text.
   * @property {codioIDE.coachBot.Callback} callback - Called on button click
   */
  register: (id, name, callback) => {
    if (!_.isFunction(callback)) {
      throw new Error('Callback should be a function')
    }
    emit.register({
      id,
      name,
      callback
    })
  },
  /**
   * Deregister coach bot menu item
   * @memberof codioIDE.coachBot
   * @method deregister
   * @param {string} id - action id to deregister
   */
  deregister: (id) => {
    emit.deregister(id)
  },
  /**
   * Show custom message in the coach bot
   * @memberof codioIDE.coachBot
   * @method write
   * @param {string} text - plain text
   * @param {codioIDE.coachBot.MESSAGE_ROLES} [ownerType] - message type, default is ASSISTANT
   */
  write: (text, ownerType) => {
    emit.write({ text, ownerType })
  },

  /**
   * Show custom button in the coach bot
   * @memberof codioIDE.coachBot
   * @method showButton
   * @param {string} text - Button text.
   * @param {codioIDE.coachBot.Callback} callback - button on click handler
   */
  showButton: (text, callback) => {
    if (!_.isFunction(callback)) {
      throw new Error('Callback should be a function')
    }
    emit.showButton({ text, callback })
  },

  /**
   * Get coach bot context
   * @memberof codioIDE.coachBot
   * @method getContext
   * @async
   * @return {Promise<codioIDE.coachBot.CoachBotContext>}
   */
  getContext: async () => {
    return new Promise((resolve, reject) => {
      emit.getContext((result) =>
        result.error ? reject(result.error) : resolve(result.context)
      )
    })
  },
  /**
   * Ask bot
   * @memberof codioIDE.coachBot
   * @method ask
   * @async
   * @param {codioIDE.coachBot.CoachBotCustomRequestData} data - request data
   * @param {codioIDE.coachBot.CoachBotCustomRequestOptions|AskBotDoneCallback} options - request options or done callback
   * @param {codioIDE.coachBot.AskBotDoneCallback} [onDone] - on request done callback
   * @return {Promise}
   */
  ask: async (data, options, onDone) => {
    if (data.messages) {
      if (
        data.messages.findIndex(
          (message) => message.role === MESSAGE_OWNER.BOT
        ) === 0
      ) {
        throw new Error('first message must use the USER role')
      }
      let currentMessageRole
      data.messages.forEach((message) => {
        if (message.role === currentMessageRole) {
          throw new Error('roles must alternate between USER and ASSISTANT')
        }
        currentMessageRole = message.role
      })
    }

    return new Promise((resolve, reject) => {
      emit.ask({
        data,
        options,
        onDone: (result) => {
          onDone && onDone(result)
          result?.error ? reject(result.error) : resolve(result)
        }
      })
    })
  },

  /**
   * Show user input
   * @memberof codioIDE.coachBot
   * @method input
   * @async
   * @param {string} text - text that would be shown before input
   * @param {string} defaultText - prepopulate text for the input
   * @return {Promise<string>} - user input
   */
  input: async (text, defaultText) => {
    return new Promise((resolve, reject) => {
      emit.input({
        text,
        defaultText,
        callback: ({ error, input }) => {
          error ? reject(error) : resolve(input)
        }
      })
    })
  },
  /**
   * Get history messages
   * @memberof codioIDE.coachBot
   * @method getHistory
   * @async
   * @returns {Promise<codioIDE.coachBot.CoachBotMessage[]>}
   */
  getHistory: async () => {
    return new Promise((resolve, reject) => {
      emit.getHistory((result) =>
        result.error ? reject(result.error) : resolve(result.messages)
      )
    })
  },
  /**
   * Show coach bot menu
   * @memberof codioIDE.coachBot
   * @method showMenu
   */
  showMenu: () => {
    emit.showMenu()
  },
  /**
   * Get LLM proxy details
   * @memberof codioIDE.coachBot
   * @method getLlmProxyDetails
   * @async
   * @returns {Promise<codioIDE.coachBot.LlmProxyDetails[]>}
   */
  getLlmProxyDetails: async () => {
    return new Promise((resolve, reject) => {
      emit.getLlmProxyDetails((result) => {
        result.error ? reject(result.error) : resolve(result.detailsList)
      })
    })
  },
  /**
   * @memberof codioIDE.coachBot
   * @readonly
   * @enum {string}
   */
  MESSAGE_ROLES: {
    USER: MESSAGE_OWNER.USER,
    ASSISTANT: MESSAGE_OWNER.BOT
  }
}