nrn-agents
Version:
A library for creating and deploying gaming agents at scale
1,484 lines (1,464 loc) • 67.7 kB
JavaScript
// lib/feature-engineering/angles.ts
var getAngleBetweenPoints = (point1, point2) => {
if (point2.x === point1.x && point2.y === point1.y) {
return 0;
}
const upperBound = Math.atan(Infinity);
const slope = -(point2.y - point1.y) / (point2.x - point1.x);
var newAngle = Math.abs(Math.atan(slope) - upperBound) / upperBound * 90;
if (point2.x < point1.x) {
newAngle += 180;
}
return newAngle;
};
// lib/feature-engineering/clusters.ts
var getCenterPosition = (entities) => {
const center = { x: 0, y: 0 };
entities.forEach((entity) => {
center.x += entity.x;
center.y += entity.y;
});
center.x /= entities.length;
center.y /= entities.length;
return center;
};
// lib/feature-engineering/raycast.ts
var degreeToRadians = (angle) => {
return angle * (Math.PI / 180);
};
var calculateDistance = (point1, point2) => {
return Math.hypot(point2.x - point1.x, point2.y - point1.y);
};
var rayIntersectsRect = (origin, direction, rect) => {
let { x: cx, y: cy, width, height } = rect;
let { x: ox, y: oy } = origin;
let { x: dx, y: dy } = direction;
let rx = cx - width / 2;
let ry = cy - height / 2;
let t1 = (rx - ox) / dx;
let t2 = (rx + width - ox) / dx;
let t3 = (ry - oy) / dy;
let t4 = (ry + height - oy) / dy;
let tmin = Math.max(Math.min(t1, t2), Math.min(t3, t4));
let tmax = Math.min(Math.max(t1, t2), Math.max(t3, t4));
if (tmax < 0) return null;
if (tmin > tmax) return null;
return { x: ox + tmin * dx, y: oy + tmin * dy };
};
var castRay = (origin, colliders, angle, invertY = false) => {
const rayDir = {
x: Math.cos(degreeToRadians(angle)),
y: (invertY ? -1 : 1) * Math.sin(degreeToRadians(angle))
};
let closestIntersection = null;
colliders.forEach((collider) => {
const intersection = rayIntersectsRect(origin, rayDir, collider);
if (intersection) {
if (!closestIntersection || Math.hypot(intersection.x - origin.x, intersection.y - origin.y) < Math.hypot(closestIntersection.x - origin.x, closestIntersection.y - origin.y)) {
closestIntersection = intersection;
}
}
});
return closestIntersection !== null ? {
...closestIntersection,
angle,
distance: calculateDistance(origin, closestIntersection),
intersects: true
} : { angle, intersects: false };
};
var raycast2d = (origin, colliders, angles, invertY = false) => {
return angles.map((angle) => {
return castRay(origin, colliders, angle, invertY);
});
};
var getRayAngles = (numRays = 8) => {
const incrementalAngle = 360 / numRays;
return [...Array(numRays).keys()].map((i) => incrementalAngle * i);
};
// lib/modules/feature-engineering.ts
var FeatureEngineering = class _FeatureEngineering {
static numFeatures;
// The number of features defined in the state config.
static stateConfig = [];
// Static array to store the state configuration for features.
/** Mapping of feature types to their respective processing functions. */
static conversionFunctions = {
cosineSimilarity: this.getCosineSimilarity,
raycast: this.getRaycasts,
angle: this.getAngle,
relativePosition: this.getRelativePosition,
relativePositionToCluster: this.getRelativePositionToCluster,
onehot: this.getOneHotEncoding,
binary: this.getBinary,
rescale: this.getRescaledValue,
normalize: this.getNormalizedValue
};
/** Number of features returned from feature engineering functions (-1 for dynamic size). */
static featureSizes = {
cosineSimilarity: 1,
raycast: -1,
angle: 2,
relativePosition: 3,
relativePositionToCluster: 3,
onehot: -1,
binary: 1,
rescale: 1,
normalize: 1
};
/** Required data keys and setup for each feature type. */
static requiredData = {
cosineSimilarity: {
keys: { vector1: true, vector2: true }
},
raycast: {
keys: { origin: true, colliders: true, maxDistance: true },
setup: { numRays: false }
},
angle: {
keys: { entity1: true, entity2: true }
},
relativePosition: {
keys: { entity1: true, entity2: true, maxDistance: true }
},
relativePositionToCluster: {
keys: { origin: true, clusterEntities: true, maxDistance: true }
},
onehot: {
keys: { value: true },
setup: { options: true }
},
binary: {
keys: { value: true },
setup: { operator: true, comparison: true }
},
rescale: {
keys: { value: true, scaleFactor: true }
},
normalize: {
keys: { value: true },
setup: { mean: true, stdev: true }
}
};
/**
* Sets the state configuration for feature extraction.
* @param config - The array of feature configurations.
*/
static setStateConfig(config = []) {
this.numFeatures = this._validateStateConfig(config);
_FeatureEngineering.stateConfig = config;
}
/**
* Validate that the state configuration is correct.
* @param config - The array of feature configurations.
* @returns The number of features in the state config
*/
static _validateStateConfig(config) {
let numFeatures = 0;
config.forEach((featureConfig) => {
const configData = this.requiredData[featureConfig.type];
if (configData === void 0) {
throw Error(`'${featureConfig.type}' is not a valid feature type`);
}
if (featureConfig.keys === void 0) {
throw Error("'keys' is missing from state configuration");
}
Object.keys(featureConfig.keys).forEach((key) => {
if (configData.keys[key] === void 0) {
throw Error(`'${key}' is not a valid key`);
}
});
Object.keys(configData.keys).forEach((key) => {
if (configData.keys[key] && featureConfig.keys[key] === void 0) {
throw Error(`'${key}' is missing from 'keys', but it is required`);
}
});
if (configData.setup) {
Object.keys(configData.setup).forEach((key) => {
if (configData.setup[key] && featureConfig.setup?.[key] === void 0) {
throw Error(`'${key}' is missing from 'setup', but it is required`);
}
});
}
if (this.featureSizes[featureConfig.type] !== -1) {
numFeatures += this.featureSizes[featureConfig.type];
} else if (featureConfig.type === "raycast") {
numFeatures += featureConfig.setup?.numRays ?? 8;
} else if (featureConfig.type === "onehot") {
numFeatures += featureConfig.setup.options.length;
}
});
return numFeatures;
}
/**
* Validate that the key exists in the game world.
* @param value - Value extracted from the world.
* @param key The key used to extract a value.
*/
static _validateKeyInWorld(value, key) {
if (value === void 0) {
throw Error(`'${key}' does not exist in the world object`);
}
}
/**
* Extracts a value from the game world.
* @param world - The world object containing the data for feature extraction.
* @param key - Key to extract feature object.
* @returns Object that will be used in feature engineering
*/
static _parseWorldWithKey(world, key) {
const keySplit = key.split(/\.|\[|\]/).filter(Boolean);
if (keySplit.length > 1) {
return keySplit.reduce((feature, k) => {
if (feature && feature[k] !== void 0) {
return feature[k];
}
throw Error(`Invalid key: ${key}`);
}, world);
}
return world[key];
}
/**
* Extracts the state features from the world object based on the current state configuration.
* @param world - The world object containing the data for feature extraction.
* @returns An array of feature values.
*/
static getState(world) {
const state = [];
this.stateConfig.forEach((featureConfig) => {
const featureObject = {};
Object.keys(featureConfig.keys).forEach((featureKey) => {
featureObject[featureKey] = this._parseWorldWithKey(world, featureConfig.keys[featureKey]);
this._validateKeyInWorld(featureObject[featureKey], featureConfig.keys[featureKey]);
});
const featureConfigSetup = featureConfig.setup;
if (featureConfigSetup !== void 0) {
Object.keys(featureConfigSetup).forEach((setupKey) => {
featureObject[setupKey] = featureConfigSetup[setupKey];
});
}
const feature = _FeatureEngineering.conversionFunctions[featureConfig.type](
featureObject
);
state.push(...feature);
});
return state;
}
static _dotProduct(A, B) {
if (A.length !== B.length) {
throw new Error("Vectors are not the same dimensions");
}
return A.reduce((sum, value, i) => sum + value * B[i], 0);
}
static _normL2(vector) {
return Math.sqrt(_FeatureEngineering._dotProduct(vector, vector));
}
/**
* Processes the cosine similarity of two vectors.
* @param params - Parameters required for cosine similarity.
* @returns The cosine similarity.
*/
static getCosineSimilarity({ vector1, vector2 }) {
const denom = _FeatureEngineering._normL2(vector1) * _FeatureEngineering._normL2(vector2);
return [_FeatureEngineering._dotProduct(vector1, vector2) / denom];
}
/**
* Processes raycast features based on the provided parameters.
* @param params - Parameters required for raycasting.
* @returns An array of raycast results.
*/
static getRaycasts({ origin, colliders, maxDistance, numRays = 8 }) {
const angles = getRayAngles(numRays);
const rays = raycast2d(origin, colliders, angles);
return rays.map((ray) => {
if (ray.intersects) {
return 1 - Math.min(ray.distance / maxDistance, 1);
} else {
return -1;
}
});
}
/**
* Processes the angle between an origin and another entity.
* @param params - Parameters for angle calculation.
* @returns An array containing the sine and cosine of the angle between the entities.
*/
static getAngle({ entity1, entity2 }) {
const angleDegrees = getAngleBetweenPoints(entity1, entity2);
const angleRadians = angleDegrees / 360 * Math.PI * 2;
return [Math.sin(angleRadians), Math.cos(angleRadians)];
}
/**
* Processes the relative position between an origin and another entity.
* @param params - Parameters for relative position calculation.
* @returns An array containing the distance and directional components.
*/
static getRelativePosition({ entity1, entity2, maxDistance }) {
const squaredDistance = (entity2.x - entity1.x) ** 2 + (entity2.y - entity1.y) ** 2;
const distance = Math.sqrt(squaredDistance) / maxDistance;
const [angleSin, angleCos] = _FeatureEngineering.getAngle({ entity1, entity2 });
return [distance, angleSin, angleCos];
}
/**
* Processes relative position features between an origin and a cluster of objects.
* @param params - Parameters for relative position calculation.
* @returns An array containing the distance and directional components.
*/
static getRelativePositionToCluster({ origin, clusterEntities, maxDistance }) {
const clusterCenter = getCenterPosition(clusterEntities);
return _FeatureEngineering.getRelativePosition({
entity1: origin,
entity2: clusterCenter,
maxDistance
});
}
/**
* Processes one-hot encoding for a given value and set of options.
* @param params - Parameters for one-hot encoding.
* @returns An array representing the one-hot encoded value.
*/
static getOneHotEncoding({ value, options }) {
return options.map((x) => x === value ? 1 : 0);
}
/**
* Processes binary features based on a comparison operation.
* @param params - Parameters for the binary operation.
* @returns An array with the result of the comparison (1 or 0).
*/
static getBinary({ value, operator, comparison }) {
let conditionMet = false;
if (operator === "=") {
conditionMet = value === comparison;
} else if (operator === ">") {
conditionMet = value > comparison;
} else if (operator === "<") {
conditionMet = value < comparison;
} else if (operator === "!=") {
conditionMet = value !== comparison;
}
return [conditionMet ? 1 : 0];
}
/**
* Processes rescaled values.
* @param params - Parameters for rescaling.
* @returns An array with the rescaled value.
*/
static getRescaledValue({ value, scaleFactor }) {
return [value / scaleFactor];
}
/**
* Processes normalized values.
* @param params - Parameters for normalization.
* @returns An array with the normalized value.
*/
static getNormalizedValue({ value, mean, stdev }) {
return [(value - mean) / stdev];
}
};
var feature_engineering_default = FeatureEngineering;
// lib/agent-wrappers/agent-wrapper-core.ts
var AgentWrapperCore = class {
numAgents;
model;
frameDelay;
forcedHold;
pressActions;
holdActions;
forceHoldActions;
continuousActions;
actionToHead;
inputs;
inputCooldown;
previousHoldActions;
lockedAction;
constructor(model, config = {}, numAgents = 1) {
this.numAgents = numAgents;
this.model = model;
this.frameDelay = config.frameDelay ?? 15;
this.forcedHold = config.forcedHold ?? 4;
this.pressActions = [];
this.holdActions = config.holdActions ?? [];
this.forceHoldActions = config.forceHoldActions ?? [];
this.forceHoldActions.forEach((x) => {
if (!this.holdActions.includes(x)) {
this.holdActions.push(x);
}
});
this.actionToHead = {};
this.continuousActions = {};
Object.keys(model.config.actionMetadata).forEach((actionGroup) => {
model.config.actionMetadata[actionGroup].order.forEach((action) => {
if (model.config.actionMetadata[actionGroup].actionType === "continuous") {
this.continuousActions[action] = true;
}
this.actionToHead[action] = actionGroup;
if (!this.holdActions.includes(action)) {
this.pressActions.push(action);
}
});
});
this.reset();
}
reset() {
this.inputs = Array.from({ length: this.numAgents }, () => ({}));
this.inputCooldown = Array.from({ length: this.numAgents }, () => ({}));
this.previousHoldActions = Array.from({ length: this.numAgents }, () => ({}));
this.lockedAction = Array.from({ length: this.numAgents }, () => ({ name: "", head: "", cooldown: 0 }));
for (let i = 0; i < this.numAgents; i++) {
Object.keys(this.model.config.actionMetadata).forEach((actionGroup) => {
this.model.config.actionMetadata[actionGroup].order.forEach((action) => {
this.inputs[i][action] = false;
this.inputCooldown[i][action] = 0;
if (this.holdActions.includes(action)) {
this.previousHoldActions[i][action] = false;
}
});
});
}
}
applyPressCooldown(inputs, action, agentIdx = 0) {
if (this.continuousActions[action]) return;
if (this.inputCooldown[agentIdx][action] > 0) {
inputs[action] = false;
this.inputCooldown[agentIdx][action] -= 1;
} else if (inputs[action]) {
this.inputCooldown[agentIdx][action] = this.frameDelay;
}
}
applyHoldCooldown(inputs, action, agentIdx = 0) {
if (this.continuousActions[action]) return;
const cooldownBool = this.inputCooldown[agentIdx][action] > 0;
if (cooldownBool) {
inputs[action] = false;
this.inputCooldown[agentIdx][action] -= 1;
}
if (!this.previousHoldActions[agentIdx][action]) {
if (!cooldownBool && inputs[action] && this.forceHoldActions.includes(action)) {
this.lockedAction[agentIdx] = {
name: action,
head: this.actionToHead[action],
cooldown: this.forcedHold
};
}
} else if (!inputs[action]) {
this.inputCooldown[agentIdx][action] = this.frameDelay;
}
}
checkLockedAction(action, agentIdx = 0) {
if (this.lockedAction[agentIdx].cooldown > 0 && this.actionToHead[action] === this.lockedAction[agentIdx].head) {
if (action === this.lockedAction[agentIdx].name) {
this.lockedAction[agentIdx].cooldown -= 1;
if (this.lockedAction[agentIdx].cooldown === 0) {
this.lockedAction[agentIdx] = { name: "", head: "", cooldown: 0 };
}
}
return true;
}
return false;
}
copyPrevHoldActions(inputs, agentIdx = 0) {
this.model.config.actionMetadata[this.lockedAction[agentIdx].head].order.forEach((action) => {
inputs[action] = this.previousHoldActions[agentIdx][action];
});
}
applyFrameDelay(inputs, agentIdx = 0) {
this.pressActions.forEach((action) => {
this.applyPressCooldown(inputs, action, agentIdx);
});
this.holdActions.forEach((action) => {
const isLocked = this.checkLockedAction(action, agentIdx);
if (!isLocked) {
this.applyHoldCooldown(inputs, action, agentIdx);
} else if (action === this.lockedAction[agentIdx].name) {
this.copyPrevHoldActions(inputs, agentIdx);
}
});
}
trackPreviousHoldInputs(inputs, agentIdx = 0) {
this.holdActions.forEach((action) => {
this.previousHoldActions[agentIdx][action] = inputs[action];
});
}
isActionLocked(agentIdx = 0) {
return this.lockedAction[agentIdx].cooldown > 0;
}
_selectAction(selectionInputs, selectionFunction, agentIdx = 0) {
this.trackPreviousHoldInputs(this.inputs[agentIdx], agentIdx);
let inputs;
if (selectionFunction !== void 0) {
inputs = selectionFunction.call(this, selectionInputs);
} else {
inputs = this.model.selectAction(selectionInputs);
}
this.applyFrameDelay(inputs, agentIdx);
this.inputs[agentIdx] = inputs;
return inputs;
}
};
// lib/agent-wrappers/probabilistic-agent-wrapper.ts
var JOINT_ACTION_DELIMITER = "@";
var ProbabilisticAgentWrapper = class _ProbabilisticAgentWrapper extends AgentWrapperCore {
numSamples;
framesRemaining;
previousPolicy;
currentAction;
actionSubkeys;
policySimilarityThreshold;
constructor(model, config = {}, numAgents = 1) {
super(model, config, numAgents);
this.numSamples = {};
this.framesRemaining = {};
this.previousPolicy = {};
this.currentAction = {};
this.actionSubkeys = {};
model.outputGroups.forEach((actionGroup) => {
const rawNumSamples = config.numSamples?.[actionGroup] ?? 1;
const metadata = model.config.actionMetadata[actionGroup];
if (rawNumSamples > 1) {
if (metadata.policyMapping !== "probabilisticSampling") {
throw Error(
`policyMapping for '${actionGroup}' must be 'probabilisticSampling' to use numSamples > 1`
);
} else if (metadata.actionType !== "discrete") {
throw Error(`Cannot use numSamples > 1 for '${metadata.actionType}' action spaces`);
} else if (metadata.order.filter((x) => this.holdActions.includes(x)).length === 0) {
throw Error(
`Some actions in the '${actionGroup}' action head must be included in 'holdActions' to use numSamples > 1`
);
} else if (metadata.order.filter((x) => this.forceHoldActions.includes(x)).length === metadata.order.length) {
throw Error(
`All actions in the '${actionGroup}' action head are currently included in 'forceActions' - this is incompatible with numSamples > 1`
);
}
}
this.numSamples[actionGroup] = rawNumSamples;
this.framesRemaining[actionGroup] = Array.from({ length: numAgents }, () => 0);
this.previousPolicy[actionGroup] = Array.from({ length: numAgents }, () => null);
this.currentAction[actionGroup] = Array.from({ length: numAgents }, () => []);
this.actionSubkeys[actionGroup] = [];
});
this.policySimilarityThreshold = config.policySimilarityThreshold ?? 0.8;
}
static policySimilarity(A, B) {
return 1 - A.reduce((sum, value, i) => sum + Math.abs(value - B[i]), 0);
}
static getMostCommonAction(array) {
if (array.length === 0) return null;
let modeMap = {};
let maxEl = array[0];
let maxCount = 1;
for (let i = 0; i < array.length; i++) {
let el = array[i];
if (modeMap[el] === void 0) {
modeMap[el] = 0;
}
modeMap[el]++;
if (modeMap[el] > maxCount) {
maxEl = el;
maxCount = modeMap[el];
}
}
return { action: maxEl, count: maxCount };
}
monteCarloSampling(probabilities, actionKey, row = 0) {
const actionArray = [];
for (let i = 0; i < this.numSamples[actionKey]; i++) {
actionArray.push(this.model.selectActionOneHead(probabilities, actionKey, row));
}
return actionArray;
}
convertInputToString(input, actionKey) {
if (this.actionSubkeys[actionKey].length === 0) {
this.actionSubkeys[actionKey] = Object.keys(input).map((x) => x);
}
const actionArray = this.actionSubkeys[actionKey].filter((dir) => input[dir]);
return actionArray.length > 0 ? actionArray.sort().join(JOINT_ACTION_DELIMITER) : void 0;
}
assignSampledInput(actionKey, agentIdx = 0) {
const inputs = {};
this.actionSubkeys[actionKey].forEach((inputKey) => {
inputs[inputKey] = this.currentAction[actionKey][agentIdx].includes(inputKey);
});
return inputs;
}
sampleAction(probabilities, actionKey, agentIdx = 0, row = 0) {
if (this.numSamples[actionKey] > 1) {
let policyChange = false;
if (this.previousPolicy[actionKey][agentIdx] !== null) {
const similarity = _ProbabilisticAgentWrapper.policySimilarity(
probabilities[actionKey][row],
this.previousPolicy[actionKey][agentIdx]
);
policyChange = similarity < this.policySimilarityThreshold;
}
if (this.framesRemaining[actionKey][agentIdx] === 0 || policyChange) {
const actionArray = this.monteCarloSampling(probabilities, actionKey, row);
const { action, count } = _ProbabilisticAgentWrapper.getMostCommonAction(
actionArray.map((input) => {
return this.convertInputToString(input, actionKey);
})
);
this.currentAction[actionKey][agentIdx] = action ? action.split(JOINT_ACTION_DELIMITER) : [];
this.framesRemaining[actionKey][agentIdx] = count;
this.previousPolicy[actionKey][agentIdx] = probabilities[actionKey][row];
}
this.framesRemaining[actionKey][agentIdx] -= 1;
return this.assignSampledInput(actionKey, agentIdx);
} else {
return this.model.selectActionOneHead(probabilities, actionKey, row);
}
}
forceNoAction(actionGroup) {
const inputs = {};
this.model.config.actionMetadata[actionGroup].order.forEach((actionKey) => {
inputs[actionKey] = false;
});
return inputs;
}
// Should I write an override to check above? But we need the state...
// isActionLocked() {
// }
selectionFunction({ probabilities, agentIdx = 0, row = 0 }) {
let selection = {};
this.model.outputGroups.forEach((actionGroup) => {
if (this.lockedAction[agentIdx].cooldown - 1 > 0) {
if (this.lockedAction[agentIdx].cooldown === this.forcedHold) {
const lockedDelta = this.framesRemaining[actionGroup][agentIdx] - this.lockedAction[agentIdx].cooldown;
this.framesRemaining[actionGroup][agentIdx] = Math.max(lockedDelta + 1, 0);
}
} else {
selection = { ...selection, ...this.sampleAction(probabilities, actionGroup, agentIdx, row) };
}
});
return selection;
}
};
// lib/agent-wrappers/single-agent-wrapper.ts
var SingleAgentWrapper = class extends ProbabilisticAgentWrapper {
constructor(model, config = {}) {
super(model, config);
}
selectAction(input) {
const probabilities = this.model.getProbabilities(input);
return this._selectAction({ probabilities, agentIdx: 0 }, this.selectionFunction);
}
};
// lib/modules/agents/general-agent-core.ts
import {
TabularModel,
NeuralNetworkMultihead,
DataCollector
} from "nrn-ml";
// lib/modules/helpers/state-validator.ts
var StateValidator = class {
modelTypes = [];
expectedInputDims = [];
addInputDimValidation(configs) {
for (let idx = 0; idx < configs.length; idx++) {
this.expectedInputDims[idx] = configs[idx].inputDim || configs[idx].nFeatures || configs[idx].numDiscreteStates;
if (this.expectedInputDims[idx] === void 0) {
throw Error("Expected input dimensionality for state validation is 'undefined'");
}
}
}
validateState(inputs, idx = 0) {
if (idx > this.modelTypes.length) {
throw Error("'idx' out of range for 'modelTypes'");
}
if (idx > this.expectedInputDims.length) {
throw Error("'idx' out of range for 'expectedInputDims'");
}
if (this.modelTypes[idx] === "neural-network") {
const inputs1D = inputs;
if (this._isArray1D(inputs1D)) {
if (inputs1D.length !== this.expectedInputDims[idx]) {
throw Error(`Input dimensionality mismatch: Received (${inputs1D.length}) vs Expected (${this.expectedInputDims[idx]})`);
}
return [inputs1D];
} else if (this._isArray2D(inputs)) {
const inputs2D = inputs;
inputs2D.forEach((inputRow) => {
if (inputRow.length !== this.expectedInputDims[idx]) {
throw Error(`Input dimensionality mismatch: Received (${inputRow.length}) vs Expected (${this.expectedInputDims[idx]})`);
}
});
} else {
throw Error("'neural-network' model state must be a 1D array or Matrix");
}
} else if (this.modelTypes[idx] === "simple") {
const inputsNumber = inputs;
if (!Number.isInteger(inputsNumber)) {
throw Error("'simple' model state must be an integer");
} else if (inputsNumber < 0 || inputsNumber >= this.expectedInputDims[idx]) {
throw Error(`Cell ${inputsNumber} is incompatable with model with cells between 0-${this.expectedInputDims[idx] - 1}`);
}
}
return inputs;
}
getEmptyState(idx = 0) {
if (this.modelTypes[idx] === "simple") {
return -1;
} else if (this.modelTypes[idx] === "neural-network") {
return new Array(this.expectedInputDims[idx]).fill(0);
}
}
_isArray1D(x) {
const isArray = Array.isArray(x);
const is1D = isArray && !Array.isArray(x[0]);
return isArray && is1D;
}
_isArray2D(x) {
const isArray = Array.isArray(x);
const is2D = isArray && Array.isArray(x[0]) && !Array.isArray(x[0][0]);
return isArray && is2D;
}
};
var state_validator_default = StateValidator;
// lib/modules/agents/general-agent-core.ts
var modelsMapping = {
"simple": TabularModel,
"neural-network": NeuralNetworkMultihead
// "hierarchical": HierarchicalNeuralNetwork,
};
var DEFAULT_AGENT_WRAPPER = { useAgentWrapper: false };
var GeneralAgentCore = class extends state_validator_default {
numAgents;
initializedBool;
agentConfigs;
dataCollectors;
initialModelConfigs;
actionConfig;
constructor(numAgents = [1]) {
super();
if (!Array.isArray(numAgents)) {
throw Error("'numAgents' must be an array");
}
numAgents.forEach((num) => {
if (num < 1 || !Number.isInteger(num)) {
throw Error("Elements in 'numAgents' must be a positive integer");
}
});
this.numAgents = numAgents;
this.initializedBool = false;
this.initializeDataCollectors();
this.agentConfigs = Array.from({ length: numAgents.length }, () => DEFAULT_AGENT_WRAPPER);
this.initialModelConfigs = new Array(numAgents.length);
this.modelTypes = new Array(numAgents.length);
this.actionConfig = {
heads: [],
metadata: {}
};
}
_setAgentConfig(agentConfig, idx = 0) {
this.agentConfigs[idx] = agentConfig;
}
initializeDataCollectors() {
this.dataCollectors = [];
for (let i = 0; i < this.numAgents.length; i++) {
this.dataCollectors.push([]);
for (let j = 0; j < this.numAgents[i]; j++) {
this.dataCollectors[i].push(new DataCollector());
}
}
}
validateModelType(modelConfig, idx = 0) {
this.initialModelConfigs[idx] = modelConfig;
if (modelsMapping[modelConfig.modelType] === void 0) {
throw Error("Invalid model type");
}
this.modelTypes[idx] = modelConfig.modelType;
}
addValidation(configs) {
if (configs.length !== this.numAgents.length) {
throw Error(`Incorrect number of 'configs'. Expecting ${this.numAgents.length}`);
}
this.addInputDimValidation(configs);
for (let idx = 0; idx < this.numAgents.length; idx++) {
for (let j = 0; j < this.numAgents[idx]; j++) {
this.dataCollectors[idx][j].addValidationParams(
configs[idx],
this.modelTypes[idx]
);
}
}
if (this.actionConfig.heads.length === 0) {
this.actionConfig = {
heads: configs[0].actionHeads,
metadata: configs[0].actionMetadata
};
}
}
setCollectionInterval(interval) {
this.dataCollectors.forEach((collectorGroup) => {
collectorGroup.forEach((collector) => {
collector.setFrameInterval(interval);
});
});
}
setCollectionRewardThreshold(threshold) {
this.dataCollectors.forEach((collectorGroup) => {
collectorGroup.forEach((collector) => {
collector.setRewardThreshold(threshold);
});
});
}
collect(dataInstance, groupIdx = 0, agentIdx = 0) {
dataInstance.action = this.convertActionsForCollect(dataInstance.action);
return this.dataCollectors[groupIdx][agentIdx].collect(dataInstance);
}
getTrainingData(groupIdx = 0, agentIdx = 0) {
return this.dataCollectors[groupIdx][agentIdx].trainingData;
}
clearTrainingData() {
this.dataCollectors.forEach((collectorGroup) => {
collectorGroup.forEach((collector) => {
collector.reset();
});
});
}
createContinuousActionArray(rawAction, actionGroup) {
return this.actionConfig.metadata[actionGroup].order.map((name) => rawAction[name]);
}
convertActionToOneHot(rawAction, actionGroup) {
return [this.actionConfig.metadata[actionGroup].order.map((name) => {
return rawAction[name] ? 1 : 0;
})];
}
convertActionsForCollect(rawAction) {
if (!Array.isArray(rawAction)) {
const validTypes = { discrete: "boolean", continuous: "number" };
const action = {};
this.actionConfig.heads.forEach((actionGroup) => {
const { actionType, order } = this.actionConfig.metadata[actionGroup];
const sampleAction = rawAction[order[0]];
if (typeof sampleAction === validTypes[actionType]) {
action[actionGroup] = actionType === "discrete" ? this.convertActionToOneHot(rawAction, actionGroup) : this.createContinuousActionArray(rawAction, actionGroup);
}
});
return action;
}
return rawAction;
}
convertToAnalog(action, allowInsideCircle = false) {
const { x, y } = action;
if (x === void 0 || y === void 0) {
throw Error("'x' and 'y' must be defined in the action");
}
const magnitude = Math.sqrt(x ** 2 + y ** 2);
let scalingFactor = 1 / magnitude;
if (allowInsideCircle) {
scalingFactor = Math.min(1, scalingFactor);
}
return {
x: x * scalingFactor,
y: y * scalingFactor
};
}
_getNewModel(initialModelConfig, idx = 0) {
if (initialModelConfig === void 0) {
throw Error("Model config is not defined");
}
return new modelsMapping[this.modelTypes[idx]]({ config: initialModelConfig });
}
_getNewAgent(initialModelConfig, idx = 0) {
const model = this._getNewModel(initialModelConfig);
const agent = new SingleAgentWrapper(model, this.agentConfigs[idx]);
return [model, agent];
}
_createModel(modelData, idx = 0) {
this.validateModelType(modelData.config, idx);
if (modelData.parameters === void 0) {
const newModel = this._getNewModel(modelData.config, idx);
return { createdNewModel: true, model: newModel };
} else {
return { createdNewModel: false, model: new modelsMapping[this.modelTypes[idx]](modelData) };
}
}
_createAgent(modelData, agentConfig, idx = 0) {
this.agentConfigs[idx] = agentConfig;
const { createdNewModel, model } = this._createModel(modelData, idx);
return { createdNewModel, model, agent: new SingleAgentWrapper(model, agentConfig) };
}
};
// lib/modules/agents/agent-core.ts
var AgentCore = class extends GeneralAgentCore {
model;
trainedModel;
agent;
trainedAgent;
constructor(agentConfig = { useAgentWrapper: false }) {
super();
this.agentConfigs[0] = agentConfig;
}
setAgentConfig(agentConfig, policyMapping = {}) {
if (this.model) {
this._setAgentConfig(agentConfig);
Object.keys(policyMapping).forEach((actionHead) => {
const headMetadata = this.actionConfig.metadata[actionHead];
if (headMetadata) {
headMetadata.policyMapping = policyMapping[actionHead];
}
});
this.agent = new SingleAgentWrapper(this.model, agentConfig);
}
}
createAgent(modelData) {
const { createdNewModel, model, agent } = this._createAgent(modelData, this.agentConfigs[0]);
this.model = model;
this.agent = agent;
this.addValidation([this.model.config]);
this.initializedBool = true;
return createdNewModel;
}
getProbabilities(inputs, postTrainingBool = false) {
inputs = this.validateState(inputs);
const model = postTrainingBool && this.trainedModel ? this.trainedModel : this.model;
return model.getProbabilities(inputs);
}
selectAction(inputs, postTrainingBool = false) {
inputs = this.validateState(inputs);
if (postTrainingBool && this.trainedModel) {
if (this.agentConfigs[0]?.useAgentWrapper && this.trainedAgent) {
return this.trainedAgent.selectAction(inputs);
} else {
return this.trainedModel.selectAction(inputs);
}
}
if (this.agentConfigs[0]?.useAgentWrapper) {
return this.agent.selectAction(inputs);
} else {
return this.model.selectAction(inputs);
}
}
clearInputTracker() {
this.agent.reset();
}
};
var agent_core_default = AgentCore;
// lib/modules/helpers/api-client.ts
import axios from "axios";
// lib/constants.ts
var API_BASE_URL = "https://api.nrnagents.ai";
var API_VERSION = "v1";
var API_URL = `${API_BASE_URL}/${API_VERSION}`;
var SDK_NAME = "nrn-agents";
var SDK_LANGUAGE = "javascript";
var SDK_VERSION = "0.2.3";
// lib/utils/error-handler.ts
var colors = {
red: "\x1B[31m",
yellow: "\x1B[33m",
white: "\x1B[37m",
gray: "\x1B[90m",
blue: "\x1B[34m",
reset: "\x1B[0m"
};
var SDKError = class extends Error {
constructor(code, message, status = null, details = null) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.status = status;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
toString() {
const output = [];
output.push(`${colors.red}
=== ${this.name} ===${colors.reset}`);
output.push(`${colors.yellow}Code: ${this.code}${colors.reset}`);
output.push(`${colors.white}Message: ${this.message}${colors.reset}`);
if (this.status) {
output.push(`${colors.gray}Status: ${this.status}${colors.reset}`);
}
if (this.details) {
output.push(`${colors.gray}Details:${colors.reset}`);
output.push(`${colors.gray}${JSON.stringify(this.details, null, 2)}${colors.reset}`);
}
if (this.stack) {
output.push(`${colors.blue}
Stack Trace:${colors.reset}`);
const stackLines = this.stack.split("\n").slice(1);
output.push(`${colors.gray}${stackLines.join("\n")}${colors.reset}`);
}
output.push(`${colors.red}${"=".repeat(20)}${colors.reset}`);
return output.join("\n");
}
toJSON() {
return {
name: this.name,
code: this.code,
message: this.message,
status: this.status,
details: this.details,
stack: this.stack
};
}
};
var APIError = class extends SDKError {
constructor(response) {
const { status } = response;
const { code, message, details, validationErrors } = response.data.error || {};
if (status === 400 && validationErrors) {
super("VALIDATION_ERROR", "Validation failed", status, {
validationErrors,
details
});
} else {
super(code || "API_ERROR", message || "Unknown API Error", status, details);
}
}
};
var NetworkError = class extends SDKError {
constructor(message, details = null) {
super("NETWORK_ERROR", message, null, {
...details,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
};
var ConfigurationError = class extends SDKError {
constructor(message, details = null) {
super("CONFIGURATION_ERROR", message, null, {
...details,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
};
var TimeoutError = class extends SDKError {
constructor(message = "Request timed out", details = null) {
super("TIMEOUT_ERROR", message, null, {
...details,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
});
}
};
// lib/modules/helpers/api-client.ts
var APIClient = class _APIClient {
static instance = null;
apiKey;
session;
isValidSession;
useCookieAuth;
gameId;
game;
isValidGame;
chunkingThreshold;
backend;
client;
constructor() {
if (_APIClient.instance) {
return _APIClient.instance;
}
this.apiKey = "";
this.session = {};
this.isValidSession = false;
this.useCookieAuth = false;
this.gameId = "";
this.game = {};
this.isValidGame = false;
this.chunkingThreshold = 1e4;
this._createClient(API_URL);
_APIClient.instance = this;
}
static getInstance() {
if (!_APIClient.instance) {
_APIClient.instance = new _APIClient();
}
return _APIClient.instance;
}
_createClient(backend) {
this.backend = backend;
this.client = axios.create({
baseURL: this.backend,
timeout: 3e4,
withCredentials: true,
headers: {
"Content-Type": "application/json"
}
});
this._setupInterceptors();
}
_setupInterceptors() {
this.client.interceptors.request.use(
(config) => {
if (!this.apiKey && !this.useCookieAuth) {
throw new ConfigurationError("API key not set");
}
config.headers = {
...config.headers,
...!this.useCookieAuth && { "x-api-key": this.apiKey },
"x-sdk-name": SDK_NAME,
"x-sdk-language": SDK_LANGUAGE,
"x-sdk-version": SDK_VERSION
};
return config;
},
(error) => Promise.reject(error)
);
this.client.interceptors.response.use(
(response) => response,
(error) => this._handleError(error, true)
);
}
_handleError(error, throwError = false) {
let sdkError;
if (error.response?.data?.error) {
sdkError = new APIError(error.response);
} else if (error.code === "ERR_NETWORK") {
sdkError = new NetworkError(
"Unable to connect to the server. Please check your internet connection.",
{ originalError: error.message }
);
} else if (error.code === "ECONNABORTED") {
sdkError = new TimeoutError("The request timed out. Please try again.", {
originalError: error.message
});
} else {
sdkError = error;
}
if (throwError) {
throw sdkError;
}
return sdkError;
}
async setApiKey(apiKey) {
this.apiKey = apiKey;
return await this.validateSession();
}
async setGameId(gameId) {
this.gameId = gameId;
return await this.validateGame();
}
async overrideBackendUrl(newBackend) {
this._createClient(newBackend);
return await this.validateSession();
}
async setUseCookieAuth(useCookieAuth) {
this.useCookieAuth = useCookieAuth;
this._setupInterceptors;
return await this.validateSession();
}
setChunkingThreshold(threshold) {
this.chunkingThreshold = threshold;
}
async validateGame() {
this.isValidGame = false;
this.game = {};
if (!this.gameId) {
return false;
}
try {
const response = await this.get(`/games/${this.gameId}`);
this.game = response.data;
this.isValidGame = true;
return true;
} catch {
return false;
}
}
async validateSession() {
this.isValidSession = false;
this.session = {};
if (!this.apiKey) {
return false;
}
try {
const response = await this.get("/auth/me");
const data = response.data;
if (typeof data.id !== "string" || !data.id.startsWith("usr_") || typeof data.name !== "string" || typeof data.username !== "string" || typeof data.email !== "string" || typeof data.isRegistered !== "boolean" || typeof data.isAdmin !== "boolean") {
return false;
}
this.session = data;
this.isValidSession = true;
return true;
} catch {
return false;
}
}
async get(path, config = {}) {
return this.client.get(path, config);
}
async post(path, data = {}, config = {}) {
return this.client.post(path, data, config);
}
async put(path, data = {}, config = {}) {
return this.client.put(path, data, config);
}
async delete(path, config = {}) {
return this.client.delete(path, config);
}
async patch(path, data = {}, config = {}) {
return this.client.patch(path, data, config);
}
async chunkedUpload(endpoint, body, dataToChunk, options = {}) {
const { onProgress, retryAttempts = 3, retryDelay = 1e3 } = options;
const totalChunks = Math.ceil(dataToChunk.length / this.chunkingThreshold) || 1;
let chunkId;
let response;
for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) {
const currentChunk = dataToChunk.slice(
chunkNumber * this.chunkingThreshold,
(chunkNumber + 1) * this.chunkingThreshold
);
const chunkData = {
...body,
data: currentChunk,
totalChunks: totalChunks > 1 ? totalChunks : void 0,
chunkNumber: totalChunks > 1 ? chunkNumber : void 0,
chunkId
};
let attempts = 0;
while (attempts < retryAttempts) {
try {
response = await this.post(endpoint, chunkData);
if (chunkNumber === 0) {
chunkId = response.data.id;
}
if (onProgress) {
onProgress({
chunk: chunkNumber + 1,
totalChunks,
progress: (chunkNumber + 1) / totalChunks * 100
});
}
break;
} catch (error) {
attempts++;
if (attempts === retryAttempts) {
throw error;
}
await new Promise(
(resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempts - 1))
);
}
}
}
return response.data;
}
};
var api_client_default = APIClient;
// lib/modules/agents/base-agent.ts
var BaseAgent = class extends agent_core_default {
api;
id;
name;
architecture;
owner;
agentType;
modelData;
constructor(agentData, options = {
owner: void 0,
agentConfig: void 0,
delayInit: true
}) {
super(options.agentConfig);
this.api = api_client_default.getInstance();
this.id = agentData.id;
this.name = agentData.name;
this.architecture = agentData.architecture;
this.owner = options.owner || {
type: "user",
id: this.api.session.id
};
if (!options.delayInit) {
this.initialize();
}
this.agentType = "base";
}
setName(newName) {
this.name = newName;
}
async initialize() {
try {
const validBool = await this._getModelData();
if (validBool) {
const createdNewModel = this.createAgent(this.modelData);
delete this.modelData;
if (createdNewModel) {
console.log("CREATED NEW MODEL - SHOULD WE SAVE?");
}
} else {
throw Error("Could not fetch model");
}
} catch {
throw Error("Failed to initialize model");
}
}
_getAgentUrl() {
return `/games/${this.api.gameId}/agents/${this.id}`;
}
_getDataCreateUrl() {
return `/games/${this.api.gameId}/datasets`;
}
async _getModelData() {
try {
const modelDataResponse = await this.api.get(this._getAgentUrl());
this.name = modelDataResponse.data.name;
this.architecture = modelDataResponse.data.architecture;
if (this.architecture.config.stateSpaceConfig) {
feature_engineering_default.setStateConfig(this.architecture.config.stateSpaceConfig.config);
}
this.modelData = {};
if (modelDataResponse.data.modelData) {
if (modelDataResponse.data.modelData.parameters) {
this.modelData.parameters = modelDataResponse.data.modelData.parameters;
} else if (modelDataResponse.data.modelData.frequencies) {
this.modelData.frequencies = modelDataResponse.data.modelData.frequencies;
}
this.modelData.config = {
...modelDataResponse.data.modelData.config,
modelType: this.architecture.modelType
};
}
return true;
} catch (err) {
if (err.response?.data) console.log(err.response?.data);
return false;
}
}
async uploadData(contributionMapId = "") {
try {
await this.api.chunkedUpload(
`${this._getDataCreateUrl()}/${contributionMapId}`,
{ agentId: this.id, agentType: this.agentType },
this.getTrainingData()
);
return true;
} catch (err) {
console.log(err);
console.log(err.response?.data);
return false;
}
}
async save(newModelBool = false) {
throw Error("Still in development");
}
async delete() {
await this.api.delete(this._getAgentUrl());
}
};
var base_agent_default = BaseAgent;
// lib/modules/agents/imitation-learning-agent.ts
var ImitationLearningAgent = class extends base_agent_default {
constructor(agentData, options) {
super(agentData, options);
this.agentType = "imitation";
}
async train(trainingData, config) {
try {
const configMapping = {
"simple": { updatableCells: [], multiplier: 1 },
"neural-network": { epochs: 2500, batchSize: 32 }
};
return true;
} catch (err) {
console.log(err.response?.data);
return false;
}
}
async save(newModelBool = false) {
await super.save(newModelBool);
if (!newModelBool) {
this.discardTraining(true);
}
}
discardTraining(discardData = false) {
this.trainedModel = null;
this.trainedAgent = null;
if (discardData) {
this.clearTrainingData();
}
}
};
var imitation_learning_agent_default = ImitationLearningAgent;
// lib/modules/agents/reinforcement-learning-agent.ts
var ReinforcementLearningAgent = class extends base_agent_default {
constructor(agentData, options) {
super(agentData, options);
this.agentType = "reinforcement";
}
};
var reinforcement_learning_agent_default = ReinforcementLearningAgent;
// lib/modules/agents/demo-agent.ts
var DemoAgent = class extends agent_core_default {
constructor(modelData, agentConfig) {
super(agentConfig);
this.createAgent(modelData);
}
async downloadParameters() {
throw Error("Ability to download parameters is not implemented yet");
}
};
var demo_agent_default = DemoAgent;
// lib/modules/agents/trainable-demo-agent.ts
var TrainableDemoAgent = class extends demo_agent_default {
api;
constructor(modelData, agentConfig) {
super(modelData, agentConfig);
this.api = api_client_default.getInstance();
}
async train(config) {
const configMapping = {
simple: { updatableCells: [], multiplier: 1 },
"neural-network": { epochs: 200, batchSize: 32 }
};
try {
const modelData = {
config: { ...this.model.config, modelType: this.modelTypes[0] }
};
if (this.modelTypes[0] === "neural-network") {
modelData.parameters = this.model.parameters;
} else if (this.modelTypes[0] === "simple") {
modelData.frequencies = this.model.frequencies;
}
const trainingResponse = await this.api.post("imitation/train-demo", {
modelData,
trainingData: this.getTrainingData(),
trainingConfig: config ?? configMapping[this.modelTypes[0]]
});
this.createAgent(trainingResponse.data);
return true;
} catch (err) {
console.log(err);
return false;
}
}
};
var trainable_demo_agent_default = TrainableDemoAgent;
// lib/agent-wrappers/multi-agent-wrapper.ts
var MultiAgentWrapper = class extends ProbabilisticAgentWrapper {
staggerData;
constructor(model, numAgents, config = {}) {
super(model, config, numAgents);
this.staggerData = {
interval: -1,
cooldowns: []
};
}
setStaggerInterval(interval) {
this.staggerData = {
interval,
cooldowns: Array.from({ length: this.numAgents }, (_, idx) => idx)
};
}
getLockedAgents() {
const lockedAgents = {};
for (let agentIdx = 0; agentIdx < this.numAgents; agentIdx++) {
lockedAgents[agentIdx] = this.isActionLocked(agentIdx);
}
return lockedAgents;
}
selectAction(input, agentsToSkip = {}) {
return input.map((state, agentIdx) => {
if (this.staggerData.interval === -1 || this.staggerData.cooldowns[agentIdx] === 0) {
const probabilities = agentsToSkip[agentIdx] ? {} : this.model.getProbabilities(Array.isArray(state) ? [state] : state);
if (this.staggerData.interval !== -1) {
this.staggerData.cooldowns[agentIdx] = this.staggerData.interval;
}
return this._selectAction({ probabilities, agentIdx, row: 0 }, this.selectionFunction, agentIdx);
} else {
const action = {};
this.holdActions.forEach((actionName) => {
action[actionName] = this.previousHoldActions[agentIdx][actionName];
});
this.staggerData.cooldowns[agentIdx] -= 1