UNPKG

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.

249 lines (248 loc) 9.14 kB
/** * Internal helper to validate an array of FuzzySets. * @param {FuzzySet[]} sets */ const validateFuzzySets = sets => { if (!Array.isArray(sets)) { throw new TypeError("Parameter 'sets' must be an array."); } if (!sets.every(set => set instanceof FuzzySet)) { throw new TypeError('All elements in the array must be instances of FuzzySet.'); } }; /** * Performs defuzzification using the Centroid (Center of Gravity) method. * @param {Object.<string, number>} fuzzyOutput - Results from rule evaluation. * @param {FuzzySet[]} outputSets - The sets defining the output range. * @param {number} [step=0.5] - Resolution of the integral approximation. * @returns {number} The crisp output value. */ export const defuzzifyCentroid = (fuzzyOutput, outputSets, step = 0.5) => { validateType(fuzzyOutput, 'object', 'defuzzifyCentroid.fuzzyOutput'); for (const outputName in fuzzyOutput) { validateType(fuzzyOutput[outputName], 'number', `fuzzyOutput['${outputName}']`); } validateFuzzySets(outputSets); validateType(step, 'number', 'defuzzifyCentroid.step'); /** @type {number} - Accumulated weighted area for centroid */ let totalAreaWeighted = 0; /** @type {number} - Accumulated total area */ let totalArea = 0; // Numerical integration (Centroid approximation) for (let i = 0; i <= 100; i += step) { /** @type {number} - Maximum membership found at point x(i) */ let maxMembershipAtX = 0; outputSets.forEach(set => { /** @type {number} - Rule strength applied to the output set */ const strength = fuzzyOutput[set.name] || 0; /** @type {number} - Cut or scale the output set membership */ const membership = Math.min(strength, set.calculate(i)); maxMembershipAtX = Math.max(maxMembershipAtX, membership); }); totalAreaWeighted += i * maxMembershipAtX; totalArea += maxMembershipAtX; } return totalArea === 0 ? 0 : totalAreaWeighted / totalArea; }; /** * Utility to calculate fuzzy membership using a trapezoidal shape safely. * @param {number} value - The input value to check. * @param {number} a - Start of the rise. * @param {number} b - End of the rise (start of plateau). * @param {number} c - Start of the fall (end of plateau). * @param {number} d - End of the fall. * @param {boolean} [optimize=false] - Optimizes performance by skipping math for absolute bounds. * @returns {number} Degree of membership [0, 1]. */ export const trapezoid = (value, a, b, c, d, optimize = false) => { if (optimize) { // If the value is completely outside the outer bounds, return 0 immediately if (value <= a || value >= d) return 0; // If the value is entirely within the plateau, return 1 immediately if (value >= b && value <= c) return 1; } /** @type {number} - Safely calculate rising slope */ const rise = a === b ? 1 : (value - a) / (b - a); /** @type {number} - Safely calculate falling slope */ const fall = c === d ? 1 : (d - value) / (d - c); /** @type {number} - Internal value clamping between 0 and 1 */ const membership = Math.max(0, Math.min(rise, 1, fall)); return isNaN(membership) ? 0 : membership; }; /** * Utility to validate types and throw formatted errors, preventing code repetition. * @param {any} value - The value to evaluate. * @param {string} expectedType - The expected data type. * @param {string} paramName - The name of the parameter for the error message. */ const validateType = (value, expectedType, paramName) => { if (typeof value !== expectedType) { throw new TypeError(`Parameter '${paramName}' must be a ${expectedType}.`); } }; /** * Represents a single Membership Function (Trapezoidal). */ class FuzzySet { /** * Utility to calculate fuzzy membership using a trapezoidal shape. * @param {number} value - The input value to check. * @param {number} a - Start of the rise. * @param {number} b - End of the rise (start of plateau). * @param {number} c - Start of the fall (end of plateau). * @param {number} d - End of the fall. * @param {boolean} [optimize=false] - Performance optimization flag. * @returns {number} Degree of membership [0, 1]. */ static trapezoid(value, a, b, c, d, optimize = false) { return trapezoid(value, a, b, c, d, optimize); } /** @type {string} - Internal name of the fuzzy set */ #name = ''; /** @type {number} - Internal left foot coordinate */ #a = 0; /** @type {number} - Internal left shoulder coordinate */ #b = 0; /** @type {number} - Internal right shoulder coordinate */ #c = 0; /** @type {number} - Internal right foot coordinate */ #d = 0; /** @type {boolean} - Internal flag to enable calculation optimization */ #optimize = false; get name() { return this.#name; } set name(value) { validateType(value, 'string', 'FuzzySet.name'); this.#name = value; } get a() { return this.#a; } set a(value) { validateType(value, 'number', 'FuzzySet.a'); this.#a = value; } get b() { return this.#b; } set b(value) { validateType(value, 'number', 'FuzzySet.b'); this.#b = value; } get c() { return this.#c; } set c(value) { validateType(value, 'number', 'FuzzySet.c'); this.#c = value; } get d() { return this.#d; } set d(value) { validateType(value, 'number', 'FuzzySet.d'); this.#d = value; } get optimize() { return this.#optimize; } set optimize(value) { validateType(value, 'boolean', 'FuzzySet.optimize'); this.#optimize = value; } /** * @param {string} name - Name of the set (e.g., "Hot"). * @param {number} a - Left foot. * @param {number} b - Left shoulder. * @param {number} c - Right shoulder. * @param {number} d - Right feet. * @param {boolean} [optimize=false] - Enables performance optimization. */ constructor(name, a, b, c, d, optimize = false) { this.name = name; this.a = a; this.b = b; this.c = c; this.d = d; this.optimize = optimize; } /** * Calculates the membership degree. * @param {number} x - Crisp input. * @returns {number} */ calculate(x) { validateType(x, 'number', 'calculate.x'); return FuzzySet.trapezoid(x, this.#a, this.#b, this.#c, this.#d, this.#optimize); } } /** * The Inference Engine handles linguistic variables and defuzzification. */ class MamdaniInferenceSystem { /** @type {Map<string, FuzzySet[]>} - Storage for linguistic variables */ #variables = new Map(); /** * Registers a linguistic variable and its sets. * @param {string} name - Variable name (e.g., "temperature"). * @param {FuzzySet[]} sets - Array of fuzzy sets. */ addVariable(name, sets) { validateType(name, 'string', 'addVariable.name'); validateFuzzySets(sets); this.#variables.set(name, sets); } /** * Removes a linguistic variable. * @param {string} name - Variable name (e.g., "temperature"). * @returns {boolean} True if the element was successfully removed. */ removeVariable(name) { validateType(name, 'string', 'removeVariable.name'); return this.#variables.delete(name); } /** * Gets a linguistic variable and its sets. * @param {string} name - Variable name (e.g., "temperature"). * @returns {FuzzySet[]} */ getVariable(name) { validateType(name, 'string', 'getVariable.name'); /** @type {FuzzySet[] | undefined} - Attempted fetch from map */ const result = this.#variables.get(name); if (!result) throw new Error(`Linguistic variable '${name}' not found in the inference system.`); return [...result]; } /** * Checks if a linguistic variable exists. * @param {string} name - Variable name (e.g., "temperature"). * @returns {boolean} */ hasVariable(name) { validateType(name, 'string', 'hasVariable.name'); return this.#variables.has(name); } /** * Fuzzifies a crisp input into a map of memberships. * @param {string} varName * @param {number} value * @returns {Object.<string, number>} */ fuzzify(varName, value) { validateType(varName, 'string', 'fuzzify.varName'); validateType(value, 'number', 'fuzzify.value'); /** @type {FuzzySet[]} - The sets associated with the variable */ const sets = this.#variables.get(varName) || []; /** @type {Object.<string, number>} - The fuzzified results dictionary */ const results = {}; sets.forEach(set => { results[set.name] = set.calculate(value); }); return results; } } export { FuzzySet, MamdaniInferenceSystem };