PrithviGuardian / client /src /lib /pluginSystem.ts
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
26.2 kB
// JSON-based Plugin Support for Custom Calculations
// Extensible calculation engine with user-defined formulas and plugins
interface PluginMetadata {
id: string;
name: string;
version: string;
author: string;
description: string;
category: string;
tags: string[];
compatibility: string[];
lastUpdated: Date;
}
interface PluginInput {
name: string;
type: 'number' | 'string' | 'boolean' | 'select' | 'file';
label: string;
description?: string;
required: boolean;
defaultValue?: any;
validation?: {
min?: number;
max?: number;
pattern?: string;
options?: string[];
};
unit?: string;
}
interface PluginOutput {
name: string;
type: 'number' | 'string' | 'object' | 'array';
label: string;
description?: string;
unit?: string;
format?: string;
}
interface PluginCalculation {
formula: string;
conditions?: Array<{
condition: string;
formula: string;
}>;
constants?: Record<string, number>;
functions?: Record<string, string>;
}
interface Plugin {
metadata: PluginMetadata;
inputs: PluginInput[];
outputs: PluginOutput[];
calculations: PluginCalculation[];
documentation?: {
overview: string;
examples: Array<{
title: string;
description: string;
inputs: Record<string, any>;
expectedOutput: Record<string, any>;
}>;
references: string[];
};
}
interface PluginExecutionResult {
success: boolean;
outputs: Record<string, any>;
errors?: string[];
warnings?: string[];
executionTime: number;
pluginId: string;
}
class PluginSystem {
private plugins: Map<string, Plugin> = new Map();
private readonly PLUGIN_STORAGE_KEY = 'prithvi_plugins';
private readonly EXECUTION_TIMEOUT = 10000; // 10 seconds
constructor() {
this.loadPlugins();
this.initializeBuiltinPlugins();
}
// Plugin Management
loadPlugin(pluginData: string | Plugin): boolean {
try {
let plugin: Plugin;
if (typeof pluginData === 'string') {
plugin = JSON.parse(pluginData);
} else {
plugin = pluginData;
}
// Validate plugin structure
this.validatePlugin(plugin);
// Check for conflicts
if (this.plugins.has(plugin.metadata.id)) {
const existingPlugin = this.plugins.get(plugin.metadata.id)!;
if (existingPlugin.metadata.version >= plugin.metadata.version) {
throw new Error(`Plugin ${plugin.metadata.id} version ${plugin.metadata.version} is not newer than existing version ${existingPlugin.metadata.version}`);
}
}
// Install plugin
plugin.metadata.lastUpdated = new Date();
this.plugins.set(plugin.metadata.id, plugin);
this.savePlugins();
return true;
} catch (error) {
throw new Error(`Failed to load plugin: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
unloadPlugin(pluginId: string): boolean {
try {
if (!this.plugins.has(pluginId)) {
throw new Error(`Plugin ${pluginId} not found`);
}
this.plugins.delete(pluginId);
this.savePlugins();
return true;
} catch (error) {
throw new Error(`Failed to unload plugin: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getPlugin(pluginId: string): Plugin | undefined {
return this.plugins.get(pluginId);
}
getAllPlugins(): Plugin[] {
return Array.from(this.plugins.values()).sort((a, b) =>
a.metadata.name.localeCompare(b.metadata.name)
);
}
getPluginsByCategory(category: string): Plugin[] {
return this.getAllPlugins().filter(plugin =>
plugin.metadata.category.toLowerCase() === category.toLowerCase()
);
}
searchPlugins(query: string): Plugin[] {
if (!query || query.trim().length === 0) {
return this.getAllPlugins();
}
const searchTerm = query.toLowerCase();
return this.getAllPlugins().filter(plugin =>
plugin.metadata.name.toLowerCase().includes(searchTerm) ||
plugin.metadata.description.toLowerCase().includes(searchTerm) ||
plugin.metadata.tags.some(tag => tag.toLowerCase().includes(searchTerm))
);
}
// Plugin Execution
executePlugin(pluginId: string, inputs: Record<string, any>): Promise<PluginExecutionResult> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
try {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
reject(new Error(`Plugin ${pluginId} not found`));
return;
}
// Set execution timeout
const timeoutId = setTimeout(() => {
reject(new Error(`Plugin execution timed out after ${this.EXECUTION_TIMEOUT}ms`));
}, this.EXECUTION_TIMEOUT);
// Validate inputs
const validationResult = this.validateInputs(plugin, inputs);
if (!validationResult.valid) {
clearTimeout(timeoutId);
resolve({
success: false,
outputs: {},
errors: validationResult.errors,
executionTime: Date.now() - startTime,
pluginId
});
return;
}
// Execute calculations
const executionResult = this.executeCalculations(plugin, inputs);
clearTimeout(timeoutId);
resolve({
success: executionResult.success,
outputs: executionResult.outputs,
errors: executionResult.errors,
warnings: executionResult.warnings,
executionTime: Date.now() - startTime,
pluginId
});
} catch (error) {
reject(error);
}
});
}
// Plugin Creation Helper
createPlugin(metadata: PluginMetadata, config: {
inputs: PluginInput[];
outputs: PluginOutput[];
calculations: PluginCalculation[];
documentation?: Plugin['documentation'];
}): Plugin {
try {
const plugin: Plugin = {
metadata: {
...metadata,
lastUpdated: new Date()
},
inputs: config.inputs,
outputs: config.outputs,
calculations: config.calculations,
documentation: config.documentation
};
this.validatePlugin(plugin);
return plugin;
} catch (error) {
throw new Error(`Failed to create plugin: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Export/Import
exportPlugin(pluginId: string): string {
try {
const plugin = this.plugins.get(pluginId);
if (!plugin) {
throw new Error(`Plugin ${pluginId} not found`);
}
return JSON.stringify(plugin, null, 2);
} catch (error) {
throw new Error(`Failed to export plugin: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
exportAllPlugins(): string {
try {
const allPlugins = Array.from(this.plugins.values());
return JSON.stringify({
plugins: allPlugins,
exportDate: new Date().toISOString(),
version: '1.0'
}, null, 2);
} catch (error) {
throw new Error(`Failed to export plugins: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
importPlugins(importData: string): { successful: number; failed: number; errors: string[] } {
try {
const data = JSON.parse(importData);
const plugins = data.plugins || [data]; // Handle single plugin or multiple
let successful = 0;
let failed = 0;
const errors: string[] = [];
plugins.forEach((plugin: Plugin, index: number) => {
try {
this.loadPlugin(plugin);
successful++;
} catch (error) {
failed++;
errors.push(`Plugin ${index + 1}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
});
return { successful, failed, errors };
} catch (error) {
throw new Error(`Failed to import plugins: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Private Methods
private validatePlugin(plugin: Plugin): void {
// Validate metadata
if (!plugin.metadata || !plugin.metadata.id || !plugin.metadata.name) {
throw new Error('Plugin metadata is incomplete');
}
if (!plugin.metadata.version || !this.isValidVersion(plugin.metadata.version)) {
throw new Error('Plugin version is invalid');
}
// Validate inputs
if (!Array.isArray(plugin.inputs)) {
throw new Error('Plugin inputs must be an array');
}
plugin.inputs.forEach((input, index) => {
if (!input.name || !input.type || !input.label) {
throw new Error(`Input ${index} is incomplete`);
}
if (!['number', 'string', 'boolean', 'select', 'file'].includes(input.type)) {
throw new Error(`Input ${index} has invalid type: ${input.type}`);
}
});
// Validate outputs
if (!Array.isArray(plugin.outputs) || plugin.outputs.length === 0) {
throw new Error('Plugin must have at least one output');
}
plugin.outputs.forEach((output, index) => {
if (!output.name || !output.type || !output.label) {
throw new Error(`Output ${index} is incomplete`);
}
});
// Validate calculations
if (!Array.isArray(plugin.calculations) || plugin.calculations.length === 0) {
throw new Error('Plugin must have at least one calculation');
}
plugin.calculations.forEach((calc, index) => {
if (!calc.formula || typeof calc.formula !== 'string') {
throw new Error(`Calculation ${index} must have a valid formula`);
}
});
}
private validateInputs(plugin: Plugin, inputs: Record<string, any>): {
valid: boolean;
errors: string[];
} {
const errors: string[] = [];
plugin.inputs.forEach(inputDef => {
const value = inputs[inputDef.name];
// Check required fields
if (inputDef.required && (value === undefined || value === null || value === '')) {
errors.push(`${inputDef.label} is required`);
return;
}
if (value === undefined || value === null) {
return; // Skip validation for optional empty fields
}
// Type validation
switch (inputDef.type) {
case 'number':
if (typeof value !== 'number' || isNaN(value)) {
errors.push(`${inputDef.label} must be a valid number`);
} else {
if (inputDef.validation?.min !== undefined && value < inputDef.validation.min) {
errors.push(`${inputDef.label} must be at least ${inputDef.validation.min}`);
}
if (inputDef.validation?.max !== undefined && value > inputDef.validation.max) {
errors.push(`${inputDef.label} must be at most ${inputDef.validation.max}`);
}
}
break;
case 'string':
if (typeof value !== 'string') {
errors.push(`${inputDef.label} must be a string`);
} else {
if (inputDef.validation?.pattern) {
const regex = new RegExp(inputDef.validation.pattern);
if (!regex.test(value)) {
errors.push(`${inputDef.label} format is invalid`);
}
}
}
break;
case 'select':
if (inputDef.validation?.options && !inputDef.validation.options.includes(value)) {
errors.push(`${inputDef.label} must be one of: ${inputDef.validation.options.join(', ')}`);
}
break;
case 'boolean':
if (typeof value !== 'boolean') {
errors.push(`${inputDef.label} must be true or false`);
}
break;
}
});
return {
valid: errors.length === 0,
errors
};
}
private executeCalculations(plugin: Plugin, inputs: Record<string, any>): {
success: boolean;
outputs: Record<string, any>;
errors?: string[];
warnings?: string[];
} {
try {
const outputs: Record<string, any> = {};
const warnings: string[] = [];
for (const calculation of plugin.calculations) {
try {
// Create calculation context
const context = {
...inputs,
...(calculation.constants || {}),
Math: Math,
abs: Math.abs,
sqrt: Math.sqrt,
pow: Math.pow,
exp: Math.exp,
log: Math.log,
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
max: Math.max,
min: Math.min,
round: Math.round,
floor: Math.floor,
ceil: Math.ceil
};
// Execute formula
let formula = calculation.formula;
// Check conditions
if (calculation.conditions) {
for (const condition of calculation.conditions) {
if (this.evaluateCondition(condition.condition, context)) {
formula = condition.formula;
break;
}
}
}
// Replace variables in formula
const result = this.evaluateFormula(formula, context);
// Map results to outputs
plugin.outputs.forEach(outputDef => {
if (outputDef.name in result) {
outputs[outputDef.name] = result[outputDef.name];
} else if (typeof result === 'number' && plugin.outputs.length === 1) {
outputs[outputDef.name] = result;
}
});
} catch (error) {
warnings.push(`Calculation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
return {
success: Object.keys(outputs).length > 0,
outputs,
warnings: warnings.length > 0 ? warnings : undefined
};
} catch (error) {
return {
success: false,
outputs: {},
errors: [`Execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`]
};
}
}
private evaluateFormula(formula: string, context: Record<string, any>): any {
try {
// Basic safety checks
if (formula.includes('eval') || formula.includes('Function') || formula.includes('require')) {
throw new Error('Unsafe formula detected');
}
// Create a safe evaluation context
const safeContext = { ...context };
delete (safeContext as any).constructor;
delete (safeContext as any).__proto__;
// Simple formula evaluation using Function constructor (safer than eval)
const func = new Function(...Object.keys(safeContext), `return ${formula}`);
return func(...Object.values(safeContext));
} catch (error) {
throw new Error(`Formula evaluation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
private evaluateCondition(condition: string, context: Record<string, any>): boolean {
try {
const result = this.evaluateFormula(condition, context);
return Boolean(result);
} catch (error) {
return false;
}
}
private isValidVersion(version: string): boolean {
return /^\d+\.\d+\.\d+$/.test(version);
}
private loadPlugins(): void {
try {
const data = localStorage.getItem(this.PLUGIN_STORAGE_KEY);
if (data) {
const plugins = JSON.parse(data);
plugins.forEach((plugin: Plugin) => {
plugin.metadata.lastUpdated = new Date(plugin.metadata.lastUpdated);
this.plugins.set(plugin.metadata.id, plugin);
});
}
} catch (error) {
console.error('Failed to load plugins from storage:', error);
}
}
private savePlugins(): void {
try {
const plugins = Array.from(this.plugins.values());
localStorage.setItem(this.PLUGIN_STORAGE_KEY, JSON.stringify(plugins));
} catch (error) {
throw new Error(`Failed to save plugins: ${error instanceof Error ? error.message : 'Storage quota exceeded'}`);
}
}
private initializeBuiltinPlugins(): void {
// Water Quality Index Calculator Plugin
const wqiPlugin: Plugin = {
metadata: {
id: 'builtin_wqi_calculator',
name: 'Water Quality Index Calculator',
version: '1.0.0',
author: 'VBharat AI',
description: 'Comprehensive Water Quality Index calculation based on multiple parameters',
category: 'Water Quality',
tags: ['water', 'quality', 'index', 'pollution'],
compatibility: ['v1.0'],
lastUpdated: new Date()
},
inputs: [
{
name: 'ph',
type: 'number',
label: 'pH',
description: 'pH value of water sample',
required: true,
validation: { min: 0, max: 14 },
unit: 'pH units'
},
{
name: 'do',
type: 'number',
label: 'Dissolved Oxygen',
description: 'Dissolved oxygen concentration',
required: true,
validation: { min: 0, max: 20 },
unit: 'mg/L'
},
{
name: 'bod',
type: 'number',
label: 'BOD',
description: 'Biochemical Oxygen Demand',
required: true,
validation: { min: 0 },
unit: 'mg/L'
},
{
name: 'nitrate',
type: 'number',
label: 'Nitrate',
description: 'Nitrate concentration',
required: true,
validation: { min: 0 },
unit: 'mg/L'
},
{
name: 'phosphate',
type: 'number',
label: 'Phosphate',
description: 'Phosphate concentration',
required: true,
validation: { min: 0 },
unit: 'mg/L'
},
{
name: 'temperature',
type: 'number',
label: 'Temperature Deviation',
description: 'Temperature deviation from normal',
required: true,
validation: { min: -10, max: 10 },
unit: '°C'
}
],
outputs: [
{
name: 'wqi',
type: 'number',
label: 'Water Quality Index',
description: 'Overall water quality index value',
unit: 'WQI'
},
{
name: 'category',
type: 'string',
label: 'Quality Category',
description: 'Water quality classification'
},
{
name: 'recommendation',
type: 'string',
label: 'Recommendation',
description: 'Treatment or action recommendation'
}
],
calculations: [
{
formula: `
// Individual quality indices
var qi_ph = ph >= 6.5 && ph <= 8.5 ? 100 : max(0, 100 - abs(7 - ph) * 15);
var qi_do = min(100, (do / 14.6) * 100);
var qi_bod = max(0, 100 - (bod * 5));
var qi_nitrate = max(0, 100 - (nitrate * 2));
var qi_phosphate = max(0, 100 - (phosphate * 10));
var qi_temp = max(0, 100 - abs(temperature) * 5);
// Weights (sum = 1.0)
var w_ph = 0.15;
var w_do = 0.25;
var w_bod = 0.25;
var w_nitrate = 0.15;
var w_phosphate = 0.10;
var w_temp = 0.10;
// Weighted WQI calculation
var wqi_value = (qi_ph * w_ph + qi_do * w_do + qi_bod * w_bod +
qi_nitrate * w_nitrate + qi_phosphate * w_phosphate + qi_temp * w_temp);
// Determine category and recommendation
var category = wqi_value > 90 ? 'Excellent' :
wqi_value > 70 ? 'Good' :
wqi_value > 50 ? 'Medium' :
wqi_value > 25 ? 'Bad' : 'Very Bad';
var recommendation = wqi_value > 70 ? 'Water is suitable for drinking with standard treatment' :
wqi_value > 50 ? 'Requires advanced treatment before use' :
'Extensive treatment required, monitor pollution sources';
({ wqi: round(wqi_value * 100) / 100, category: category, recommendation: recommendation })
`,
constants: {
IDEAL_DO: 14.6,
NEUTRAL_PH: 7.0
}
}
],
documentation: {
overview: 'Calculates comprehensive Water Quality Index based on six key parameters with appropriate weightings according to Indian standards.',
examples: [
{
title: 'Clean River Water',
description: 'Typical values for unpolluted river water',
inputs: {
ph: 7.2,
do: 8.5,
bod: 2.0,
nitrate: 5.0,
phosphate: 0.1,
temperature: 1.0
},
expectedOutput: {
wqi: 85.5,
category: 'Good',
recommendation: 'Water is suitable for drinking with standard treatment'
}
}
],
references: [
'CPCB Water Quality Criteria',
'IS 10500:2012 Drinking Water Standards',
'WHO Water Quality Guidelines'
]
}
};
this.plugins.set(wqiPlugin.metadata.id, wqiPlugin);
// Structural Load Calculator Plugin
const loadPlugin: Plugin = {
metadata: {
id: 'builtin_structural_load',
name: 'Structural Load Calculator',
version: '1.0.0',
author: 'VBharat AI',
description: 'Calculate design loads for structures as per IS 875',
category: 'Structural Engineering',
tags: ['structural', 'load', 'design', 'IS875'],
compatibility: ['v1.0'],
lastUpdated: new Date()
},
inputs: [
{
name: 'dead_load',
type: 'number',
label: 'Dead Load',
description: 'Permanent structural load',
required: true,
validation: { min: 0 },
unit: 'kN/m²'
},
{
name: 'live_load',
type: 'number',
label: 'Live Load',
description: 'Variable/imposed load',
required: true,
validation: { min: 0 },
unit: 'kN/m²'
},
{
name: 'wind_load',
type: 'number',
label: 'Wind Load',
description: 'Wind pressure load',
required: false,
defaultValue: 0,
validation: { min: 0 },
unit: 'kN/m²'
},
{
name: 'seismic_load',
type: 'number',
label: 'Seismic Load',
description: 'Earthquake load',
required: false,
defaultValue: 0,
validation: { min: 0 },
unit: 'kN/m²'
},
{
name: 'load_combination',
type: 'select',
label: 'Load Combination',
description: 'Design load combination type',
required: true,
validation: {
options: ['basic', 'wind', 'seismic', 'all']
}
}
],
outputs: [
{
name: 'design_load',
type: 'number',
label: 'Design Load',
description: 'Factored design load',
unit: 'kN/m²'
},
{
name: 'safety_factor',
type: 'number',
label: 'Safety Factor',
description: 'Overall safety factor achieved'
},
{
name: 'governing_combination',
type: 'string',
label: 'Governing Combination',
description: 'Critical load combination'
}
],
calculations: [
{
formula: `
// Load combinations as per IS 875
var basic_combo = 1.5 * (dead_load + live_load);
var wind_combo = 1.2 * (dead_load + live_load + wind_load);
var seismic_combo = 1.2 * (dead_load + live_load + seismic_load);
var wind_uplift = 0.9 * dead_load + 1.5 * wind_load;
var combinations = {
'basic': basic_combo,
'wind': max(basic_combo, wind_combo, wind_uplift),
'seismic': max(basic_combo, seismic_combo),
'all': max(basic_combo, wind_combo, seismic_combo, wind_uplift)
};
var design_load_value = combinations[load_combination];
var safety_factor_value = design_load_value / (dead_load + live_load);
var governing = design_load_value === basic_combo ? 'Basic (1.5DL+1.5LL)' :
design_load_value === wind_combo ? 'Wind (1.2DL+1.2LL+1.2WL)' :
design_load_value === seismic_combo ? 'Seismic (1.2DL+1.2LL+1.2EL)' :
'Wind Uplift (0.9DL+1.5WL)';
({
design_load: round(design_load_value * 100) / 100,
safety_factor: round(safety_factor_value * 100) / 100,
governing_combination: governing
})
`
}
],
documentation: {
overview: 'Calculates structural design loads according to IS 875 load combinations for different loading scenarios.',
examples: [
{
title: 'Residential Building',
description: 'Typical residential structure loads',
inputs: {
dead_load: 4.0,
live_load: 2.0,
wind_load: 1.0,
seismic_load: 0.8,
load_combination: 'all'
},
expectedOutput: {
design_load: 9.6,
safety_factor: 1.6,
governing_combination: 'Wind (1.2DL+1.2LL+1.2WL)'
}
}
],
references: [
'IS 875-1987 Design Loads for Buildings',
'IS 1893-2016 Earthquake Resistant Design',
'IS 456-2000 Plain and Reinforced Concrete'
]
}
};
this.plugins.set(loadPlugin.metadata.id, loadPlugin);
}
}
export const pluginSystem = new PluginSystem();
export { Plugin, PluginInput, PluginOutput, PluginExecutionResult, PluginMetadata };