tiny-ai-api
Version:
A customizable and extensible client api for managing conversations and AI interactions, currently supporting the **Google Gemini** API — with flexibility to support any similar AI APIs.
1,114 lines • 70.6 kB
JavaScript
import objHash from 'object-hash';
import { EventEmitter } from 'events';
import { isJsonObject, objType } from 'tiny-essentials';
import { encode as encodeBase64 } from 'js-base64';
/**
* @typedef {Object} SessionDataContent
* @property {AIContentData[]} data
* @property {string[]} ids
* @property {{ data: Array<TokenCount>; [key: string]: * }} tokens
* @property {{ data: Array<string>; [key: string]: * }} hash
* @property {string|null} systemInstruction
* @property {{ name: string; type: string; }[]} [customList]
* @property {string|null} model
*
*/
/**
* @typedef {Record<string, any> & SessionDataContent} SessionData
*/
/**
* @typedef {Object} AiModel
* @property {any} _response
* @property {number} index
* @property {string|null} name
* @property {string|null} id
* @property {string|null} displayName
* @property {string|null} version
* @property {string|null} description
* @property {number|null} inputTokenLimit
* @property {number|null} outputTokenLimit
* @property {number|null} temperature
* @property {number|null} maxTemperature
* @property {number|null} topP
* @property {number|null} topK
* @property {string[]} [supportedGenerationMethods]
*/
/**
* @typedef {Object} AiCategory
* @property {string} category
* @property {string} displayName
* @property {number} index
* @property {AiModel[]} data
*/
/**
* Tiny AI Server Communication API
* -----------------------------
* This class is responsible for managing AI session data, including models, history, and content generation.
* The script is designed to interact with the AI API, providing a complete structure for creating user interfaces (UI) or AI-powered chatbots.
* It implements a session management system to help handle multiple different bots.
* However, this script is not optimized for efficiently handling multiple AI instances simultaneously, which may be required for high-load scenarios or when running several AI instances at once.
*
* **Note**: This script does not automatically manage or track the token count for messages. Developers need to implement their own logic to monitor and manage token usage if necessary.
*
* Documentation written with the assistance of OpenAI's ChatGPT.
*/
class TinyAiInstance {
/**
* Important instance used to make event emitter.
* @type {EventEmitter}
*/
#events = new EventEmitter();
/**
* Important instance used to make system event emitter.
* @type {EventEmitter}
*/
#sysEvents = new EventEmitter();
#sysEventsUsed = false;
/**
* Emits an event with optional arguments to all system emit.
* @param {string | symbol} event - The name of the event to emit.
* @param {...any} args - Arguments passed to event listeners.
*/
#emit(event, ...args) {
this.#events.emit(event, ...args);
if (this.#sysEventsUsed)
this.#sysEvents.emit(event, ...args);
}
/**
* Provides access to a secure internal EventEmitter for subclass use only.
*
* This method exposes a dedicated EventEmitter instance intended specifically for subclasses
* that extend the main class. It prevents subclasses from accidentally or intentionally using
* the primary class's public event system (`emit`), which could lead to unpredictable behavior
* or interference in the base class's event flow.
*
* For security and consistency, this method is designed to be accessed only once.
* Multiple accesses are blocked to avoid leaks or misuse of the internal event bus.
*
* @returns {EventEmitter} A special internal EventEmitter instance for subclass use.
* @throws {Error} If the method is called more than once.
*/
getSysEvents() {
if (this.#sysEventsUsed)
throw new Error('Access denied: getSysEvents() can only be called once. ' +
'This restriction ensures subclass event isolation and prevents accidental interference ' +
'with the main class event emitter.');
this.#sysEventsUsed = true;
return this.#sysEvents;
}
/**
* @typedef {(...args: any[]) => void} ListenerCallback
* A generic callback function used for event listeners.
*/
/**
* Sets the maximum number of listeners for the internal event emitter.
*
* @param {number} max - The maximum number of listeners allowed.
*/
setMaxListeners(max) {
this.#events.setMaxListeners(max);
}
/**
* Emits an event with optional arguments.
* @param {string | symbol} event - The name of the event to emit.
* @param {...any} args - Arguments passed to event listeners.
* @returns {boolean} `true` if the event had listeners, `false` otherwise.
*/
emit(event, ...args) {
return this.#events.emit(event, ...args);
}
/**
* Registers a listener for the specified event.
* @param {string | symbol} event - The name of the event to listen for.
* @param {ListenerCallback} listener - The callback function to invoke.
* @returns {this} The current class instance (for chaining).
*/
on(event, listener) {
this.#events.on(event, listener);
return this;
}
/**
* Registers a one-time listener for the specified event.
* @param {string | symbol} event - The name of the event to listen for once.
* @param {ListenerCallback} listener - The callback function to invoke.
* @returns {this} The current class instance (for chaining).
*/
once(event, listener) {
this.#events.once(event, listener);
return this;
}
/**
* Removes a listener from the specified event.
* @param {string | symbol} event - The name of the event.
* @param {ListenerCallback} listener - The listener to remove.
* @returns {this} The current class instance (for chaining).
*/
off(event, listener) {
this.#events.off(event, listener);
return this;
}
/**
* Alias for `on`.
* @param {string | symbol} event - The name of the event.
* @param {ListenerCallback} listener - The callback to register.
* @returns {this} The current class instance (for chaining).
*/
addListener(event, listener) {
this.#events.addListener(event, listener);
return this;
}
/**
* Alias for `off`.
* @param {string | symbol} event - The name of the event.
* @param {ListenerCallback} listener - The listener to remove.
* @returns {this} The current class instance (for chaining).
*/
removeListener(event, listener) {
this.#events.removeListener(event, listener);
return this;
}
/**
* Removes all listeners for a specific event, or all events if no event is specified.
* @param {string | symbol} [event] - The name of the event. If omitted, all listeners from all events will be removed.
* @returns {this} The current class instance (for chaining).
*/
removeAllListeners(event) {
this.#events.removeAllListeners(event);
return this;
}
/**
* Returns the number of times the given `listener` is registered for the specified `event`.
* If no `listener` is passed, returns how many listeners are registered for the `event`.
* @param {string | symbol} eventName - The name of the event.
* @param {Function} [listener] - Optional listener function to count.
* @returns {number} Number of matching listeners.
*/
listenerCount(eventName, listener) {
return this.#events.listenerCount(eventName, listener);
}
/**
* Adds a listener function to the **beginning** of the listeners array for the specified event.
* The listener is called every time the event is emitted.
* @param {string | symbol} eventName - The event name.
* @param {ListenerCallback} listener - The callback function.
* @returns {this} The current class instance (for chaining).
*/
prependListener(eventName, listener) {
this.#events.prependListener(eventName, listener);
return this;
}
/**
* Adds a **one-time** listener function to the **beginning** of the listeners array.
* The next time the event is triggered, this listener is removed and then invoked.
* @param {string | symbol} eventName - The event name.
* @param {ListenerCallback} listener - The callback function.
* @returns {this} The current class instance (for chaining).
*/
prependOnceListener(eventName, listener) {
this.#events.prependOnceListener(eventName, listener);
return this;
}
/**
* Returns an array of event names for which listeners are currently registered.
* @returns {(string | symbol)[]} Array of event names.
*/
eventNames() {
return this.#events.eventNames();
}
/**
* Gets the current maximum number of listeners allowed for any single event.
* @returns {number} The max listener count.
*/
getMaxListeners() {
return this.#events.getMaxListeners();
}
/**
* Returns a copy of the listeners array for the specified event.
* @param {string | symbol} eventName - The event name.
* @returns {Function[]} An array of listener functions.
*/
listeners(eventName) {
return this.#events.listeners(eventName);
}
/**
* Returns a copy of the internal listeners array for the specified event,
* including wrapper functions like those used by `.once()`.
* @param {string | symbol} eventName - The event name.
* @returns {Function[]} An array of raw listener functions.
*/
rawListeners(eventName) {
return this.#events.rawListeners(eventName);
}
/**
* @typedef {Object} AIContentData
* @property {Array<Record<'text' | 'inlineData', string | { mime_type: string, data: string } | null>>} parts
* @property {string|undefined} [role]
* @property {string|number|undefined} [finishReason]
*/
/**
* @typedef {{ count: number|null, hide?: boolean }} TokenCount
*/
/** @type {string|null} */ #_apiKey = null;
/** @type {function|null} */ #_getModels = null;
/** @type {function|null} */ #_countTokens = null;
/** @type {function|null} */ #_genContentApi = null;
/** @type {string|null} */ #_selectedHistory = null;
/** @type {Record<string, function>} */ #_partTypes = {};
/** @type {function} */ #_insertIntoHistory = () => { };
/** @type {Record<string|number, string|{ text: string, hide?: boolean }>} */ _errorCode = {};
/** @type {string|null} */ _nextModelsPageToken = null;
/** @type {(AiModel|AiCategory)[]} */ models = [];
/** @type {Object.<string, SessionData>} */ history = {};
_isSingle = false;
/**
* Creates an instance of the TinyAiInstance class.
* Initializes internal variables, sets up initial configurations for handling AI models,
* session history, and content generation, with the option to use a single or multiple instances.
*
* @param {boolean} [isSingle] - If true, configures the instance to handle a single session only.
*/
constructor(isSingle = false) {
this._isSingle = isSingle;
/**
* Updates an existing entry in the session history.
*
* @param {string} id - The session identifier.
* @param {Record<string, any>} data - Data fields to update within the session.
* @returns {boolean} True if the update succeeded, false otherwise.
*/
this.#_insertIntoHistory = function (id, data) {
if (typeof id === 'string' && this.history[id]) {
for (const where in data) {
this.history[id][where] = data[where];
}
return true;
}
return false;
};
/**
* Parsers for different part types.
* @type {{ text: (input: any) => string|null, inlineData: (input: any) => { mime_type: string, data: string }|null }}
*/
this.#_partTypes = {
text: (/** @type {string} */ text) => (typeof text === 'string' ? text : null),
inlineData: (/** @type {{ mime_type: string; data: string; }} */ data) => {
if (typeof data.mime_type === 'string' && typeof data.data === 'string')
return data;
return null;
},
};
// Is single instance
if (this._isSingle) {
this.startDataId('main', true);
// @ts-ignore
this.startDataId = null;
// @ts-ignore
this.stopDataId = null;
// @ts-ignore
this.selectDataId = null;
}
}
/**
* Capitalizes the first letter of the provided string.
*
* @param {string} str - The input string to capitalize.
* @returns {string} The string with the first character in uppercase.
*/
#capitalizeFirstLetter(str) {
if (!str)
return '';
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Sets a custom value in the selected session history.
*
* @param {string} name - The name of the custom value to set.
* @param {*} value - The value to be assigned to the custom key.
* @param {number} [tokenAmount] - The token amount associated with the custom value (optional).
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @throws {Error} If the custom value name is invalid (not a non-empty string) or conflicts with existing data.
* @returns {void} This method does not return a value.
*/
setCustomValue(name, value, tokenAmount, id) {
if (typeof name === 'string' && name.length > 0 && name !== 'customList') {
// Prepare value to send
const sendValue = { [name]: value };
// This value is extremely important for the import process to identify which custom values are being used
const selectedId = this.getId(id);
if (selectedId && this.history[selectedId]) {
const history = this.history[selectedId];
if (!Array.isArray(history.customList))
history.customList = [];
// Validate the custom value
if (value !== null) {
const props = history.customList.find((/** @type {*} */ item) => item.name === name);
if (!props || typeof props.type !== 'string' || typeof props.name !== 'string') {
if (typeof history[name] === 'undefined')
history.customList.push({
name,
// @ts-ignore
type: objType(value),
});
else
throw new Error('This value name is already being used!');
}
else if (props.type !== objType(value))
throw new Error(`Invalid custom value type! ${name}: ${props.type} === ${objType(value)}`);
}
// Add Tokens
if (typeof tokenAmount === 'number')
this.history[selectedId].tokens[name] = tokenAmount;
// Send custom value into the history
if (value !== null) {
this.#_insertIntoHistory(selectedId, sendValue);
this.history[selectedId].hash[name] = objHash(value);
}
// Complete
this.#emit(`set${this.#capitalizeFirstLetter(name)}`, value, selectedId);
return;
}
}
throw new Error('Invalid custom value!');
}
/**
* Resets a custom value in the selected session history.
*
* @param {string} name - The name of the custom value to reset.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @throws {Error} If the custom value name is invalid or does not match an existing entry.
* @returns {void} This method does not return a value.
*/
resetCustomValue(name, id) {
if (typeof name === 'string' && name.length > 0 && name !== 'customList') {
// Prepare value to send
const sendValue = { [name]: null };
// This value is extremely important for the import process to identify which custom values are being used
const selectedId = this.getId(id);
if (selectedId && this.history[selectedId]) {
const history = this.history[selectedId];
if (!Array.isArray(history.customList))
history.customList = [];
// Validate the custom value
const props = history.customList.find((/** @type {*} */ item) => item.name === name);
if (isJsonObject(props) &&
typeof props.type === 'string' &&
typeof props.name === 'string') {
// Reset Tokens
if (typeof this.history[selectedId].tokens[name] !== 'undefined')
delete this.history[selectedId].tokens[name];
// Reset custom value
this.#_insertIntoHistory(selectedId, sendValue);
if (typeof this.history[selectedId].hash[name] !== 'undefined')
delete this.history[selectedId].hash[name];
// Complete
this.#emit(`set${this.#capitalizeFirstLetter(name)}`, null, selectedId);
return;
}
}
throw new Error('Invalid custom value data type!');
}
throw new Error('Invalid custom value!');
}
/**
* Completely removes a custom value from the selected session history.
*
* @param {string} name - The name of the custom value to erase.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @throws {Error} If the custom value name is invalid or does not exist.
* @returns {void} This method does not return a value.
*/
eraseCustomValue(name, id) {
this.resetCustomValue(name, id);
const history = this.getData(id);
if (history && history.customList) {
const index = history.customList.findIndex((item) => item.name === name);
if (index > -1)
history.customList.splice(index, 1);
return;
}
throw new Error('Invalid custom value!');
}
/**
* Retrieves a custom value from the selected session history.
*
* @param {string} name - The name of the custom value to retrieve.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {*} The value associated with the specified name, or `null` if it does not exist.
*/
getCustomValue(name, id) {
const history = this.getData(id);
return history && typeof history[name] !== 'undefined' && history[name] !== null
? history[name]
: null;
}
/**
* Retrieves the list of custom values from the selected session history.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {Array<*>} An array of custom values if available, or an empty array if no custom values exist.
*/
getCustomValueList(id) {
const history = this.getData(id);
return history && Array.isArray(history.customList) ? history.customList : [];
}
/**
* Set the maximum output tokens setting for an AI session.
*
* @param {number} value - The maximum number of output tokens to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setMaxOutputTokens(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { maxOutputTokens: value });
this.#emit('setMaxOutputTokens', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the maximum output tokens setting for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The maximum output tokens value, or null if not set.
*/
getMaxOutputTokens(id) {
const history = this.getData(id);
return history && typeof history.maxOutputTokens === 'number' ? history.maxOutputTokens : null;
}
/**
* Set the AI temperature setting for a session.
*
* @param {number} value - The temperature value to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setTemperature(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { temperature: value });
this.#emit('setTemperature', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the AI temperature setting for a session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The temperature value, or null if not set.
*/
getTemperature(id) {
const history = this.getData(id);
return history && typeof history.temperature ? history.temperature : null;
}
/**
* Set the top-p (nucleus sampling) value in an AI session.
*
* @param {number} value - The top-p value to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setTopP(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { topP: value });
this.#emit('setTopP', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the top-p (nucleus sampling) setting for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The top-p value, or null if not set.
*/
getTopP(id) {
const history = this.getData(id);
return history && typeof history.topP === 'number' ? history.topP : null;
}
/**
* Set the top-k setting for an AI session.
*
* @param {number} value - The top-k value to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setTopK(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { topK: value });
this.#emit('setTopK', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the top-k setting for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The top-k value, or null if not set.
*/
getTopK(id) {
const history = this.getData(id);
return history && typeof history.topK === 'number' ? history.topK : null;
}
/**
* Set the presence penalty setting for an AI session.
*
* @param {number} value - The presence penalty value to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setPresencePenalty(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { presencePenalty: value });
this.#emit('setPresencePenalty', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the presence penalty setting for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The presence penalty value, or null if not set.
*/
getPresencePenalty(id) {
const history = this.getData(id);
return history && typeof history.presencePenalty === 'number' ? history.presencePenalty : null;
}
/**
* Set the frequency penalty setting for an AI session.
*
* @param {number} value - The frequency penalty value to be set.
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setFrequencyPenalty(value, id) {
if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { frequencyPenalty: value });
this.#emit('setFrequencyPenalty', value, selectedId);
return;
}
throw new Error('Invalid number value!');
}
/**
* Get the frequency penalty setting for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {number | null} The frequency penalty value, or null if not set.
*/
getFrequencyPenalty(id) {
const history = this.getData(id);
return history && typeof history.frequencyPenalty === 'number'
? history.frequencyPenalty
: null;
}
/**
* Set the setting for enabling enhanced civic answers in an AI session.
*
* @param {boolean} value - Whether to enable enhanced civic answers (true or false).
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setEnabledEnchancedCivicAnswers(value, id) {
if (typeof value === 'boolean') {
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { enableEnhancedCivicAnswers: value });
this.#emit('setEnabledEnchancedCivicAnswers', value, selectedId);
return;
}
throw new Error('Invalid boolean value!');
}
/**
* Get the setting for whether enhanced civic answers are enabled in an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {boolean | null} The value indicating whether enhanced civic answers are enabled, or null if not set.
*/
isEnabledEnchancedCivicAnswers(id) {
const history = this.getData(id);
return history && typeof history.enableEnhancedCivicAnswers === 'boolean'
? history.enableEnhancedCivicAnswers
: null;
}
/**
* Set the model for an AI session.
*
* @param {string} data - The model to be set (must be a string).
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {void} This function does not return a value.
*/
setModel(data, id) {
const model = typeof data === 'string' ? data : null;
const selectedId = this.getId(id);
this.#_insertIntoHistory(selectedId, { model });
this.#emit('setModel', model, selectedId);
}
/**
* Get the model for an AI session.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session will be used.
* @returns {string | null} The model, or null if not set.
*/
getModel(id) {
const history = this.getData(id);
return history && typeof history.model === 'string' ? history.model : null;
}
/**
* Build content data for an AI session.
*
* @param {Array<*>} [contents] - An optional array to which the built content data will be pushed.
* @param {Record<string, any>} item - The item containing content parts or a content object.
* @param {string|null} [role] - The role to be associated with the content (optional).
* @param {boolean} [rmFinishReason=false] - If true, removes the `finishReason` property from the content.
* @returns {AIContentData|number} The constructed content data object, or array length if pushed to an array.
*/
buildContents(contents, item = {}, role = null, rmFinishReason = false) {
// Content Data
const tinyThis = this;
/** @type {{ finishReason: string|number|undefined, parts: any[], role: string|undefined }} */
const contentData = { parts: [], finishReason: undefined, role: undefined };
// Role
if (typeof role === 'string')
contentData.role = role;
/** @param {Record<string, any>} content */
const insertPart = (content) => {
/** @type {Record<string, function>} */
const tinyResult = {};
for (const valName in content) {
if (typeof tinyThis.#_partTypes[valName] === 'function')
tinyResult[valName] = tinyThis.#_partTypes[valName](content[valName]);
}
contentData.parts.push(tinyResult);
};
if (Array.isArray(item.parts)) {
for (const index in item.parts)
insertPart(item.parts[index]);
}
else if (item.content)
insertPart(item.content);
if (!rmFinishReason)
if (typeof item.finishReason === 'string' || typeof item.finishReason === 'number')
contentData.finishReason = item.finishReason;
// Complete
if (Array.isArray(contents))
return contents.push(contentData);
return contentData;
}
/**
* Set the API key for the AI session.
*
* @param {string} apiKey - The API key to be set.
* @returns {void} This function does not return a value.
*/
setApiKey(apiKey) {
this.#_apiKey = typeof apiKey === 'string' ? apiKey : null;
}
/**
* Set the token for the next page of models in the AI session.
*
* @param {string} nextModelsPageToken - The token for the next models page.
* @returns {void} This function does not return a value.
*/
_setNextModelsPageToken(nextModelsPageToken) {
this._nextModelsPageToken =
typeof nextModelsPageToken === 'string' ? nextModelsPageToken : null;
}
/**
* Set the function to retrieve models for the AI session.
*
* @param {Function} getModels - The function to retrieve models.
* @returns {void} This function does not return a value.
*/
_setGetModels(getModels) {
this.#_getModels = typeof getModels === 'function' ? getModels : null;
}
/**
* Get a list of models for the AI session.
*
* @param {number} [pageSize=50] - The number of models to retrieve per page. Defaults to 50.
* @param {string|null} [pageToken=null] - The token for the next page of models, if available. Defaults to null.
* @returns {Array<*>} The list of models retrieved.
* @throws {Error} If no model list API function is defined.
*/
getModels(pageSize = 50, pageToken = null) {
if (typeof this.#_getModels === 'function')
return this.#_getModels(this.#_apiKey, pageSize, pageToken || this._nextModelsPageToken);
throw new Error('No model list api script defined.');
}
/**
* Get the list of models for the AI session.
*
* @returns {(AiModel|AiCategory)[]} The list of models.
*/
getModelsList() {
return Array.isArray(this.models) ? this.models : [];
}
/**
* Get model data from the list of models.
*
* @param {string} id - The model data id to search for in the models list.
* @returns {AiModel|null} The model data if found, otherwise null.
*/
getModelData(id) {
// @ts-ignore
const model = this.models.find((item) => item.id === id);
// @ts-ignore
if (model)
return model;
else {
for (const index in this.models) {
// @ts-ignore
if (this.models[index].category) {
// @ts-ignore
const modelCategory = this.models[index].data.find((item) => item.id === id);
if (modelCategory)
return modelCategory;
}
}
}
return null;
}
/**
* Check if a model exists in the model list.
*
* @param {string} id - The model id to check for in the models list.
* @returns {boolean} True if the model exists, false otherwise.
*/
existsModel(id) {
return this.getModelData(id) ? true : false;
}
/**
* Insert a new model into the AI session's models list.
* If the model already exists, it will not be inserted again.
*
* @param {Object} model - The model to insert.
* @param {*} model._response - The raw response.
* @param {number} model.index - The index position.
* @param {string} model.id - The unique identifier for the model.
* @param {string} [model.name] - The name of the model.
* @param {string} [model.displayName] - The display name of the model.
* @param {string} [model.version] - The version of the model.
* @param {string} [model.description] - A description of the model.
* @param {number} [model.inputTokenLimit] - The input token limit for the model.
* @param {number} [model.outputTokenLimit] - The output token limit for the model.
* @param {number} [model.temperature] - The temperature setting for the model.
* @param {number} [model.maxTemperature] - The maximum temperature setting for the model.
* @param {number} [model.topP] - The top P setting for the model.
* @param {number} [model.topK] - The top K setting for the model.
* @param {Array<string>} [model.supportedGenerationMethods] - The generation methods supported by the model.
* @param {Object} [model.category] - The category of the model.
* @param {string} model.category.id - The unique identifier for the category.
* @param {string} model.category.displayName - The display name of the category.
* @param {number} model.category.index - The index of the category.
* @returns {Record<string, any>|null} The inserted model data, or null if the model already exists.
*/
_insertNewModel(model) {
if (!isJsonObject(model))
throw new Error('Model data must be a valid object.');
// @ts-ignore
if (this.models.findIndex((item) => item.id === model.id) < 0) {
/** @type {AiModel} */
const newData = {
_response: model._response,
index: typeof model.index === 'number' ? model.index : 9999999,
name: typeof model.name === 'string' ? model.name : null,
id: typeof model.id === 'string' ? model.id : null,
displayName: typeof model.displayName === 'string' ? model.displayName : null,
version: typeof model.version === 'string' ? model.version : null,
description: typeof model.description === 'string' ? model.description : null,
inputTokenLimit: typeof model.inputTokenLimit === 'number' ? model.inputTokenLimit : null,
outputTokenLimit: typeof model.outputTokenLimit === 'number' ? model.outputTokenLimit : null,
temperature: typeof model.temperature === 'number' ? model.temperature : null,
maxTemperature: typeof model.maxTemperature === 'number' ? model.maxTemperature : null,
topP: typeof model.topP === 'number' ? model.topP : null,
topK: typeof model.topK === 'number' ? model.topK : null,
};
// Supported generation methods
if (Array.isArray(model.supportedGenerationMethods)) {
newData.supportedGenerationMethods = [];
for (const index in model.supportedGenerationMethods) {
if (typeof model.supportedGenerationMethods[index] === 'string')
newData.supportedGenerationMethods.push(model.supportedGenerationMethods[index]);
}
}
// Is category
if (model.category &&
typeof model.category.id === 'string' &&
typeof model.category.displayName === 'string' &&
typeof model.category.index === 'number') {
// Check category
/** @type {AiCategory|null} */
// @ts-ignore
let category = this.models.find((item) => item.category === model.category.id);
// Insert new category
if (!category) {
category = {
category: model.category.id,
displayName: model.category.displayName,
index: model.category.index,
data: [],
};
this.models.push(category);
}
// Compare function that sorts objects by their `index` property.
category.data.push(newData);
category.data.sort(
/** @param {{ index: number, [key: string]: any }} a @param {{ index: number, [key: string]: any }} b */
(a, b) => a.index - b.index);
}
// Normal mode
else
this.models.push(newData);
// Sort data
this.models.sort((a, b) => a.index - b.index);
return newData;
}
return null;
}
/**
* Sets a function to handle the count of tokens in the AI session.
* If a valid function is provided, it will be used to count tokens.
*
* @param {Function} countTokens - The function that will handle the token count.
* @throws {Error} Throws an error if the provided value is not a function.
* @returns {void}
*/
_setCountTokens(countTokens) {
this.#_countTokens = typeof countTokens === 'function' ? countTokens : null;
}
/**
* Counts the tokens based on the provided data and model, using a defined token counting function.
* If the function to count tokens is not set, an error is thrown.
*
* @param {Record<string, any>} data - The data that needs to be tokenized.
* @param {string} [model] - The model to use for counting tokens. If not provided, the default model is used.
* @param {AbortController} [controller] - The controller that manages the process or settings for counting tokens.
* @throws {Error} Throws an error if no token counting function is defined.
* @returns {Record<string, any>} The count of tokens.
*/
countTokens(data, model, controller) {
if (typeof this.#_countTokens === 'function')
return this.#_countTokens(this.#_apiKey, model || this.getModel(), controller, data);
throw new Error('No count token api script defined.');
}
/**
* @typedef {{ text: string, hide?: boolean }} ErrorCode
*/
/**
* Sets the error codes for the current session.
*
* @param {Record<string|number, string|ErrorCode>} errors - The error codes to set, typically an object containing error code definitions.
* @returns {void}
*/
_setErrorCodes(errors) {
this._errorCode = errors;
}
/**
* Get error details based on the provided error code.
*
* @param {string|number} code - The error code to look up.
* @returns {ErrorCode|null} An object containing the error message, or null if no error is found.
*/
getErrorCode(code) {
if (this._errorCode) {
const errData = this._errorCode[code];
if (errData) {
if (typeof errData === 'string')
return { text: errData };
else if (isJsonObject(errData) && typeof errData.text === 'string')
return errData;
}
}
return null;
}
/**
* Sets the content generation callback function for the AI session.
*
* @param {Function} callback - The callback function that handles content generation.
* @returns {void}
*/
_setGenContent(callback) {
this.#_genContentApi = typeof callback === 'function' ? callback : null;
}
/**
* Generates content for the AI session.
*
* @param {Record<string, any>} data - The data for content generation.
* @param {string} [model] - The model to be used for content generation. If not provided, the default model is used.
* @param {AbortController} [controller] - The controller managing the content generation process.
* @param {Function} [streamCallback] - The callback function for streaming content (optional).
* @returns {Record<string, any>} The generated content returned by the API.
* @throws {Error} If no content generator API script is defined.
*/
genContent(data, model, controller, streamCallback) {
if (typeof this.#_genContentApi === 'function')
return this.#_genContentApi(this.#_apiKey, typeof streamCallback === 'function' ? true : false, data, model || this.getModel(), streamCallback, controller);
throw new Error('No content generator api script defined.');
}
/**
* Select a session history ID to set as the active session.
* If `null` is passed, it deselects the current session ID.
*
* @param {string|null} id - The session history ID to select, or `null` to deselect the current session.
* @returns {boolean} `true` if the session ID was successfully selected or deselected, `false` if the ID does not exist in history.
*/
selectDataId(id) {
if (id !== null) {
if (this.history[id]) {
this.#_selectedHistory = id;
this.#emit('selectDataId', id);
return true;
}
return false;
}
this.#_selectedHistory = null;
this.#emit('selectDataId', null);
return true;
}
/**
* Get the currently selected session history ID.
* If no ID is provided, it returns the default selected session history ID.
*
* @param {string} [id] - The session history ID to retrieve. If not provided, it uses the default selected ID.
* @returns {string|null} The selected session history ID, or `null` if no history ID is selected.
*/
getId(id) {
const result = id && !this._isSingle ? id : this.#_selectedHistory;
if (typeof result === 'string')
return result;
return null;
}
/**
* Get the data associated with a specific session history ID.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {SessionData|null} The data associated with the session ID, or `null` if no data exists for that ID.
*/
getData(id) {
const selectedId = this.getId(id);
if (selectedId && this.history[selectedId])
return this.history[selectedId];
return null;
}
/**
* Calculates the total number of tokens used for messages in the session history.
*
* This method iterates over the `tokens` array in the session history and sums the `count` of tokens
* from each message, returning the total sum. If no valid session history is found or if token data is
* missing, it will return `null`.
*
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {number|null} The total number of tokens used in the session history, or `null` if no data is available.
*/
getTotalTokens(id) {
const history = this.getData(id);
if (history) {
let result = 0;
for (const msgIndex in history.tokens.data) {
if (typeof history.tokens.data[msgIndex].count === 'number')
result += history.tokens.data[msgIndex].count;
}
for (const item in history.tokens) {
if (typeof history.tokens[item] === 'number') {
result += history.tokens[item];
}
}
return result;
}
return null;
}
/**
* Retrieves the token data for a specific message in the session history by its index.
*
* **Note**: This method does not manage the token count automatically. It assumes that token data has been added
* to the history using the `addData` method.
*
* @param {number} msgIndex - The index of the message in the session history.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {TokenCount|null} The token data associated with the message at the specified index, or `null` if the data is not found.
*/
getMsgTokensByIndex(msgIndex, id) {
const history = this.getData(id);
if (history) {
const existsIndex = this.indexExists(msgIndex, id);
if (existsIndex)
return history.tokens.data[msgIndex];
}
return null;
}
/**
* Retrieves the token data for a specific message in the session history by its message ID.
*
* **Note**: This method does not manage the token count automatically. It assumes that token data has been added
* to the history using the `addData` method.
*
* @param {string} msgId - The unique ID of the message in the session history.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {TokenCount|null} The token data associated with the message with the given ID, or `null` if the message is not found.
*/
getMsgTokensById(msgId, id) {
const history = this.getData(id);
if (history) {
const msgIndex = this.getIndexOfId(msgId);
if (msgIndex > -1)
return history.tokens.data[msgIndex];
}
return null;
}
/**
* Retrieves the hash of a message at a specified index in the selected session history.
*
* @param {number} msgIndex - The index of the message whose hash is being retrieved.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {string|null} The hash value of the message at the specified index, or null if the index is invalid or does not exist.
*/
getMsgHashByIndex(msgIndex, id) {
const history = this.getData(id);
if (history) {
const existsIndex = this.indexExists(msgIndex, id);
if (existsIndex)
return history.hash.data[msgIndex];
}
return null;
}
/**
* Retrieves the hash of a message based on its ID from the selected session history.
*
* @param {string} msgId - The ID of the message whose hash is being retrieved.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {string|null} The hash value of the message with the specified ID, or null if the message ID is invalid or does not exist.
*/
getMsgHashById(msgId, id) {
const history = this.getData(id);
if (history) {
const msgIndex = this.getIndexOfId(msgId);
if (msgIndex > -1)
return history.hash.data[msgIndex];
}
return null;
}
/**
* Checks if a specific index exists in the session history.
*
* **Note**: This method assumes that the history data is available and that the `getMsgByIndex` method is used
* to retrieve the index. If the `getMsgByIndex` method returns a valid index, this method will return `true`.
*
* @param {number} index - The index to check for existence in the session history.
* @param {string} [id] - The session ID. If omitted, the currently selected session history ID will be used.
* @returns {boolean} `true` if the index exists, otherwise `false`.
*/
indexExists(index, id) {
return this.getMsgByIndex(index, id) ? true : false;
}
/**
* Retrieve a specific data entry by its index from the session history.
*
* @param {number} index - The index of the data entry to retrieve.
* @param {string} [id] - The session ID. If omitted, the currently selected s