1009 lines
31 KiB
JavaScript
1009 lines
31 KiB
JavaScript
/**
|
|
* AdapterHub - Community Adapter Registry and Management
|
|
*
|
|
* Provides a marketplace-style interface for browsing, uploading,
|
|
* downloading, and applying community-created LoRA adapters.
|
|
*
|
|
* @module @ruvector/edge-net/models/adapter-hub
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* import { AdapterHub } from '@ruvector/edge-net/models';
|
|
*
|
|
* const hub = new AdapterHub();
|
|
*
|
|
* // Browse adapters by category
|
|
* const codeAdapters = await hub.browse({ domain: 'code', sort: 'rating' });
|
|
*
|
|
* // Download and apply an adapter
|
|
* const adapter = await hub.download('popular-code-adapter-v1');
|
|
* await myLoRA.loadAdapter(adapter);
|
|
*
|
|
* // Upload your own adapter
|
|
* await hub.upload(myLoRA, {
|
|
* name: 'My Code Assistant',
|
|
* description: 'Fine-tuned for Python coding',
|
|
* domain: 'code',
|
|
* tags: ['python', 'coding', 'assistant']
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
import { EventEmitter } from 'events';
|
|
import { createHash, randomBytes } from 'crypto';
|
|
|
|
// ============================================
|
|
// TYPE DEFINITIONS (JSDoc)
|
|
// ============================================
|
|
|
|
/**
|
|
* @typedef {Object} AdapterInfo
|
|
* @property {string} id - Unique adapter identifier
|
|
* @property {string} name - Human-readable name
|
|
* @property {string} description - Detailed description
|
|
* @property {string} author - Author name or ID
|
|
* @property {string} authorId - Unique author identifier
|
|
* @property {string} baseModel - Base model identifier
|
|
* @property {string} domain - Primary domain category
|
|
* @property {string[]} tags - Searchable tags
|
|
* @property {number} rating - Average rating (0-5)
|
|
* @property {number} ratingCount - Number of ratings
|
|
* @property {number} downloads - Total download count
|
|
* @property {number} size - Size in bytes
|
|
* @property {string} version - Adapter version
|
|
* @property {string} license - License identifier
|
|
* @property {number} createdAt - Creation timestamp
|
|
* @property {number} updatedAt - Last update timestamp
|
|
* @property {Object} config - LoRA configuration
|
|
* @property {Object} stats - Training statistics
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} BrowseOptions
|
|
* @property {string} [domain] - Filter by domain
|
|
* @property {string} [baseModel] - Filter by base model
|
|
* @property {string} [query] - Search query
|
|
* @property {string[]} [tags] - Filter by tags
|
|
* @property {string} [sort='downloads'] - Sort order
|
|
* @property {number} [limit=20] - Results per page
|
|
* @property {number} [offset=0] - Pagination offset
|
|
* @property {number} [minRating=0] - Minimum rating filter
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} UploadOptions
|
|
* @property {string} name - Adapter name
|
|
* @property {string} description - Adapter description
|
|
* @property {string} domain - Primary domain category
|
|
* @property {string[]} [tags=[]] - Searchable tags
|
|
* @property {string} [license='MIT'] - License identifier
|
|
* @property {boolean} [public=true] - Public visibility
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Review
|
|
* @property {string} id - Review ID
|
|
* @property {string} adapterId - Adapter ID
|
|
* @property {string} authorId - Review author ID
|
|
* @property {string} authorName - Review author name
|
|
* @property {number} rating - Rating (1-5)
|
|
* @property {string} comment - Review comment
|
|
* @property {number} createdAt - Creation timestamp
|
|
* @property {number} [helpful=0] - Helpful votes
|
|
*/
|
|
|
|
// ============================================
|
|
// CONSTANTS
|
|
// ============================================
|
|
|
|
/**
|
|
* Available domain categories for adapters
|
|
*/
|
|
export const ADAPTER_DOMAINS = {
|
|
code: {
|
|
name: 'Code & Programming',
|
|
description: 'Code generation, completion, and programming assistance',
|
|
icon: 'code',
|
|
subdomains: ['python', 'javascript', 'rust', 'go', 'sql', 'general'],
|
|
},
|
|
writing: {
|
|
name: 'Creative Writing',
|
|
description: 'Story writing, poetry, and creative content',
|
|
icon: 'pen',
|
|
subdomains: ['fiction', 'poetry', 'technical', 'copywriting', 'academic'],
|
|
},
|
|
math: {
|
|
name: 'Mathematics',
|
|
description: 'Mathematical reasoning and problem solving',
|
|
icon: 'calculator',
|
|
subdomains: ['algebra', 'calculus', 'statistics', 'geometry', 'logic'],
|
|
},
|
|
science: {
|
|
name: 'Science',
|
|
description: 'Scientific knowledge and reasoning',
|
|
icon: 'flask',
|
|
subdomains: ['physics', 'chemistry', 'biology', 'medicine', 'engineering'],
|
|
},
|
|
language: {
|
|
name: 'Language',
|
|
description: 'Language learning and translation',
|
|
icon: 'globe',
|
|
subdomains: ['translation', 'grammar', 'vocabulary', 'conversation'],
|
|
},
|
|
business: {
|
|
name: 'Business',
|
|
description: 'Business writing and analysis',
|
|
icon: 'briefcase',
|
|
subdomains: ['email', 'reports', 'analysis', 'marketing', 'legal'],
|
|
},
|
|
assistant: {
|
|
name: 'General Assistant',
|
|
description: 'General-purpose assistants and chatbots',
|
|
icon: 'robot',
|
|
subdomains: ['helpful', 'concise', 'detailed', 'friendly', 'formal'],
|
|
},
|
|
roleplay: {
|
|
name: 'Roleplay',
|
|
description: 'Character and roleplay adaptations',
|
|
icon: 'theater',
|
|
subdomains: ['characters', 'games', 'educational', 'simulation'],
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Default hub configuration
|
|
*/
|
|
const DEFAULT_HUB_CONFIG = {
|
|
apiEndpoint: 'https://hub.ruvector.dev/api',
|
|
storageEndpoint: 'https://storage.ruvector.dev',
|
|
cacheDir: '.ruvector/adapter-cache',
|
|
maxCacheSize: 500 * 1024 * 1024, // 500MB
|
|
enableOffline: true,
|
|
autoUpdate: true,
|
|
};
|
|
|
|
// ============================================
|
|
// ADAPTERHUB CLASS
|
|
// ============================================
|
|
|
|
/**
|
|
* AdapterHub - Central registry for community adapters
|
|
*
|
|
* Provides a complete ecosystem for discovering, sharing, and managing
|
|
* LoRA adapters. Supports offline caching, ratings/reviews, and version
|
|
* management.
|
|
*
|
|
* @extends EventEmitter
|
|
*/
|
|
export class AdapterHub extends EventEmitter {
|
|
/**
|
|
* Create an AdapterHub instance
|
|
*
|
|
* @param {Object} [config={}] - Hub configuration
|
|
*/
|
|
constructor(config = {}) {
|
|
super();
|
|
|
|
this.config = { ...DEFAULT_HUB_CONFIG, ...config };
|
|
this.userId = config.userId || `anon-${randomBytes(8).toString('hex')}`;
|
|
|
|
// Local cache of adapter metadata
|
|
this.cache = new Map();
|
|
|
|
// Downloaded adapters
|
|
this.downloaded = new Map();
|
|
|
|
// User's own adapters
|
|
this.myAdapters = new Map();
|
|
|
|
// Reviews cache
|
|
this.reviews = new Map();
|
|
|
|
// Stats
|
|
this.stats = {
|
|
totalBrowses: 0,
|
|
totalDownloads: 0,
|
|
totalUploads: 0,
|
|
cacheHits: 0,
|
|
cacheMisses: 0,
|
|
};
|
|
|
|
// Initialize local storage for offline mode
|
|
this._initLocalStorage();
|
|
}
|
|
|
|
/**
|
|
* Initialize local storage for offline caching
|
|
* @private
|
|
*/
|
|
async _initLocalStorage() {
|
|
try {
|
|
if (typeof localStorage !== 'undefined') {
|
|
// Browser environment
|
|
const cached = localStorage.getItem('ruvector-adapter-hub-cache');
|
|
if (cached) {
|
|
const data = JSON.parse(cached);
|
|
for (const [id, info] of Object.entries(data.adapters || {})) {
|
|
this.cache.set(id, info);
|
|
}
|
|
}
|
|
} else if (typeof process !== 'undefined') {
|
|
// Node.js environment
|
|
const fs = await import('fs/promises');
|
|
const path = await import('path');
|
|
const cacheFile = path.join(this.config.cacheDir, 'hub-cache.json');
|
|
|
|
try {
|
|
const content = await fs.readFile(cacheFile, 'utf-8');
|
|
const data = JSON.parse(content);
|
|
for (const [id, info] of Object.entries(data.adapters || {})) {
|
|
this.cache.set(id, info);
|
|
}
|
|
} catch {
|
|
// Cache file doesn't exist yet
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[AdapterHub] Failed to initialize local storage:', error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save cache to local storage
|
|
* @private
|
|
*/
|
|
async _saveLocalStorage() {
|
|
try {
|
|
const data = {
|
|
adapters: Object.fromEntries(this.cache),
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.setItem('ruvector-adapter-hub-cache', JSON.stringify(data));
|
|
} else if (typeof process !== 'undefined') {
|
|
const fs = await import('fs/promises');
|
|
const path = await import('path');
|
|
await fs.mkdir(this.config.cacheDir, { recursive: true });
|
|
const cacheFile = path.join(this.config.cacheDir, 'hub-cache.json');
|
|
await fs.writeFile(cacheFile, JSON.stringify(data, null, 2));
|
|
}
|
|
} catch (error) {
|
|
console.error('[AdapterHub] Failed to save local storage:', error.message);
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// BROWSING AND DISCOVERY
|
|
// ============================================
|
|
|
|
/**
|
|
* Browse available adapters with filtering and sorting
|
|
*
|
|
* @param {BrowseOptions} [options={}] - Browse options
|
|
* @returns {Promise<{adapters: AdapterInfo[], total: number, hasMore: boolean}>}
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* // Browse code adapters sorted by rating
|
|
* const results = await hub.browse({
|
|
* domain: 'code',
|
|
* sort: 'rating',
|
|
* limit: 10
|
|
* });
|
|
*
|
|
* for (const adapter of results.adapters) {
|
|
* console.log(`${adapter.name}: ${adapter.rating} stars`);
|
|
* }
|
|
* ```
|
|
*/
|
|
async browse(options = {}) {
|
|
const opts = {
|
|
domain: null,
|
|
baseModel: null,
|
|
query: null,
|
|
tags: [],
|
|
sort: 'downloads',
|
|
limit: 20,
|
|
offset: 0,
|
|
minRating: 0,
|
|
...options,
|
|
};
|
|
|
|
this.stats.totalBrowses++;
|
|
this.emit('browse:start', opts);
|
|
|
|
// Filter adapters from cache
|
|
let adapters = Array.from(this.cache.values());
|
|
|
|
// Apply filters
|
|
if (opts.domain) {
|
|
adapters = adapters.filter(a => a.domain === opts.domain);
|
|
}
|
|
if (opts.baseModel) {
|
|
adapters = adapters.filter(a => a.baseModel === opts.baseModel);
|
|
}
|
|
if (opts.minRating > 0) {
|
|
adapters = adapters.filter(a => a.rating >= opts.minRating);
|
|
}
|
|
if (opts.tags && opts.tags.length > 0) {
|
|
adapters = adapters.filter(a =>
|
|
opts.tags.some(tag => a.tags?.includes(tag))
|
|
);
|
|
}
|
|
if (opts.query) {
|
|
const query = opts.query.toLowerCase();
|
|
adapters = adapters.filter(a =>
|
|
a.name?.toLowerCase().includes(query) ||
|
|
a.description?.toLowerCase().includes(query) ||
|
|
a.tags?.some(t => t.toLowerCase().includes(query))
|
|
);
|
|
}
|
|
|
|
// Sort
|
|
switch (opts.sort) {
|
|
case 'rating':
|
|
adapters.sort((a, b) => (b.rating || 0) - (a.rating || 0));
|
|
break;
|
|
case 'downloads':
|
|
adapters.sort((a, b) => (b.downloads || 0) - (a.downloads || 0));
|
|
break;
|
|
case 'recent':
|
|
adapters.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
break;
|
|
case 'updated':
|
|
adapters.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
break;
|
|
case 'name':
|
|
adapters.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
|
|
break;
|
|
}
|
|
|
|
// Paginate
|
|
const total = adapters.length;
|
|
const paged = adapters.slice(opts.offset, opts.offset + opts.limit);
|
|
|
|
const result = {
|
|
adapters: paged,
|
|
total,
|
|
hasMore: opts.offset + opts.limit < total,
|
|
offset: opts.offset,
|
|
limit: opts.limit,
|
|
};
|
|
|
|
this.emit('browse:complete', result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Search adapters by query string
|
|
*
|
|
* @param {string} query - Search query
|
|
* @param {Object} [options={}] - Additional filters
|
|
* @returns {Promise<AdapterInfo[]>}
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* const results = await hub.search('python code completion');
|
|
* ```
|
|
*/
|
|
async search(query, options = {}) {
|
|
return this.browse({ ...options, query });
|
|
}
|
|
|
|
/**
|
|
* Get featured/recommended adapters
|
|
*
|
|
* @param {number} [limit=10] - Number of adapters to return
|
|
* @returns {Promise<AdapterInfo[]>}
|
|
*/
|
|
async getFeatured(limit = 10) {
|
|
const result = await this.browse({
|
|
sort: 'rating',
|
|
minRating: 4.0,
|
|
limit,
|
|
});
|
|
return result.adapters;
|
|
}
|
|
|
|
/**
|
|
* Get trending adapters (most downloaded recently)
|
|
*
|
|
* @param {number} [limit=10] - Number of adapters to return
|
|
* @returns {Promise<AdapterInfo[]>}
|
|
*/
|
|
async getTrending(limit = 10) {
|
|
const result = await this.browse({
|
|
sort: 'downloads',
|
|
limit,
|
|
});
|
|
return result.adapters;
|
|
}
|
|
|
|
/**
|
|
* Get adapters by domain category
|
|
*
|
|
* @param {string} domain - Domain category
|
|
* @param {Object} [options={}] - Additional options
|
|
* @returns {Promise<AdapterInfo[]>}
|
|
*/
|
|
async getByDomain(domain, options = {}) {
|
|
const result = await this.browse({ ...options, domain });
|
|
return result.adapters;
|
|
}
|
|
|
|
/**
|
|
* Get available domain categories with counts
|
|
*
|
|
* @returns {Promise<Array<{domain: string, count: number, info: Object}>>}
|
|
*/
|
|
async getDomains() {
|
|
const counts = {};
|
|
for (const adapter of this.cache.values()) {
|
|
counts[adapter.domain] = (counts[adapter.domain] || 0) + 1;
|
|
}
|
|
|
|
return Object.entries(ADAPTER_DOMAINS).map(([id, info]) => ({
|
|
domain: id,
|
|
count: counts[id] || 0,
|
|
...info,
|
|
}));
|
|
}
|
|
|
|
// ============================================
|
|
// DOWNLOAD AND APPLY
|
|
// ============================================
|
|
|
|
/**
|
|
* Download an adapter by ID
|
|
*
|
|
* @param {string} adapterId - Adapter identifier
|
|
* @param {Object} [options={}] - Download options
|
|
* @returns {Promise<Object>} Adapter data
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* const adapter = await hub.download('code-assistant-v2');
|
|
* await myLoRA.loadAdapter(adapter);
|
|
* ```
|
|
*/
|
|
async download(adapterId, options = {}) {
|
|
this.stats.totalDownloads++;
|
|
this.emit('download:start', { adapterId });
|
|
|
|
// Check local cache first
|
|
if (this.downloaded.has(adapterId)) {
|
|
this.stats.cacheHits++;
|
|
const cached = this.downloaded.get(adapterId);
|
|
this.emit('download:complete', { adapterId, cached: true });
|
|
return cached;
|
|
}
|
|
|
|
this.stats.cacheMisses++;
|
|
|
|
// Get adapter info
|
|
const info = this.cache.get(adapterId);
|
|
if (!info) {
|
|
throw new Error(`Adapter not found: ${adapterId}`);
|
|
}
|
|
|
|
// Simulate download (in production, would fetch from storage)
|
|
const adapterData = this._generateMockAdapter(info);
|
|
|
|
// Cache downloaded adapter
|
|
this.downloaded.set(adapterId, adapterData);
|
|
|
|
// Update download count
|
|
info.downloads = (info.downloads || 0) + 1;
|
|
this.cache.set(adapterId, info);
|
|
await this._saveLocalStorage();
|
|
|
|
this.emit('download:complete', { adapterId, cached: false, size: JSON.stringify(adapterData).length });
|
|
return adapterData;
|
|
}
|
|
|
|
/**
|
|
* Generate mock adapter data for demo purposes
|
|
* @private
|
|
*/
|
|
_generateMockAdapter(info) {
|
|
const rank = info.config?.rank || 4;
|
|
const dim = info.config?.embeddingDim || 384;
|
|
|
|
const adapters = {};
|
|
for (const module of ['query', 'value', 'key', 'dense']) {
|
|
adapters[module] = {
|
|
loraA: this._generateRandomMatrix(dim, rank),
|
|
loraB: this._generateRandomMatrix(rank, dim),
|
|
scaling: (info.config?.alpha || 8) / rank,
|
|
};
|
|
}
|
|
|
|
return {
|
|
version: '1.0.0',
|
|
format: 'microlora',
|
|
metadata: {
|
|
id: info.id,
|
|
name: info.name,
|
|
description: info.description,
|
|
baseModel: info.baseModel,
|
|
domain: info.domain,
|
|
rank: rank,
|
|
alpha: info.config?.alpha || 8,
|
|
trainingSamples: info.stats?.trainingSamples || 0,
|
|
trainingEpochs: info.stats?.trainingEpochs || 0,
|
|
createdAt: info.createdAt,
|
|
version: info.version,
|
|
},
|
|
config: info.config,
|
|
baseModel: info.baseModel,
|
|
adapters,
|
|
stats: info.stats,
|
|
createdAt: info.createdAt,
|
|
savedAt: Date.now(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate random matrix for mock data
|
|
* @private
|
|
*/
|
|
_generateRandomMatrix(rows, cols) {
|
|
const matrix = [];
|
|
const std = Math.sqrt(2 / (rows + cols)) * 0.1;
|
|
for (let i = 0; i < rows; i++) {
|
|
const row = [];
|
|
for (let j = 0; j < cols; j++) {
|
|
row.push((Math.random() - 0.5) * 2 * std);
|
|
}
|
|
matrix.push(row);
|
|
}
|
|
return matrix;
|
|
}
|
|
|
|
/**
|
|
* Check if an adapter is downloaded locally
|
|
*
|
|
* @param {string} adapterId - Adapter identifier
|
|
* @returns {boolean}
|
|
*/
|
|
isDownloaded(adapterId) {
|
|
return this.downloaded.has(adapterId);
|
|
}
|
|
|
|
/**
|
|
* Remove a downloaded adapter from local cache
|
|
*
|
|
* @param {string} adapterId - Adapter identifier
|
|
*/
|
|
removeDownloaded(adapterId) {
|
|
this.downloaded.delete(adapterId);
|
|
this.emit('adapter:removed', { adapterId });
|
|
}
|
|
|
|
// ============================================
|
|
// UPLOAD AND SHARE
|
|
// ============================================
|
|
|
|
/**
|
|
* Upload an adapter to the hub
|
|
*
|
|
* @param {Object} adapter - MicroLoRA instance or adapter data
|
|
* @param {UploadOptions} options - Upload options
|
|
* @returns {Promise<AdapterInfo>} Uploaded adapter info
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* const info = await hub.upload(myLoRA, {
|
|
* name: 'Python Code Assistant',
|
|
* description: 'Specialized for Python coding tasks',
|
|
* domain: 'code',
|
|
* tags: ['python', 'coding', 'assistant']
|
|
* });
|
|
* console.log(`Uploaded: ${info.id}`);
|
|
* ```
|
|
*/
|
|
async upload(adapter, options) {
|
|
const opts = {
|
|
name: 'Untitled Adapter',
|
|
description: '',
|
|
domain: 'general',
|
|
tags: [],
|
|
license: 'MIT',
|
|
public: true,
|
|
...options,
|
|
};
|
|
|
|
this.stats.totalUploads++;
|
|
this.emit('upload:start', { name: opts.name });
|
|
|
|
// Get adapter data
|
|
let adapterData;
|
|
if (typeof adapter.saveAdapter === 'function') {
|
|
adapterData = await adapter.saveAdapter();
|
|
} else {
|
|
adapterData = adapter;
|
|
}
|
|
|
|
// Generate unique ID
|
|
const id = `${opts.domain}-${opts.name.toLowerCase().replace(/\s+/g, '-')}-${randomBytes(4).toString('hex')}`;
|
|
|
|
// Create adapter info
|
|
const info = {
|
|
id,
|
|
name: opts.name,
|
|
description: opts.description,
|
|
author: this.userId,
|
|
authorId: this.userId,
|
|
baseModel: adapterData.baseModel || 'unknown',
|
|
domain: opts.domain,
|
|
tags: opts.tags,
|
|
rating: 0,
|
|
ratingCount: 0,
|
|
downloads: 0,
|
|
size: JSON.stringify(adapterData).length,
|
|
version: adapterData.version || '1.0.0',
|
|
license: opts.license,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
config: adapterData.config,
|
|
stats: adapterData.stats,
|
|
public: opts.public,
|
|
};
|
|
|
|
// Store in cache and my adapters
|
|
this.cache.set(id, info);
|
|
this.myAdapters.set(id, { info, data: adapterData });
|
|
await this._saveLocalStorage();
|
|
|
|
this.emit('upload:complete', info);
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* Update an uploaded adapter
|
|
*
|
|
* @param {string} adapterId - Adapter to update
|
|
* @param {Object} adapter - New adapter data
|
|
* @param {Object} [options={}] - Update options
|
|
* @returns {Promise<AdapterInfo>}
|
|
*/
|
|
async update(adapterId, adapter, options = {}) {
|
|
const existing = this.myAdapters.get(adapterId);
|
|
if (!existing) {
|
|
throw new Error(`Adapter not found in your uploads: ${adapterId}`);
|
|
}
|
|
|
|
let adapterData;
|
|
if (typeof adapter.saveAdapter === 'function') {
|
|
adapterData = await adapter.saveAdapter();
|
|
} else {
|
|
adapterData = adapter;
|
|
}
|
|
|
|
// Update info
|
|
const info = {
|
|
...existing.info,
|
|
...options,
|
|
updatedAt: Date.now(),
|
|
size: JSON.stringify(adapterData).length,
|
|
config: adapterData.config,
|
|
stats: adapterData.stats,
|
|
};
|
|
|
|
// Increment version
|
|
const versionParts = (info.version || '1.0.0').split('.').map(Number);
|
|
versionParts[2]++;
|
|
info.version = versionParts.join('.');
|
|
|
|
// Update cache
|
|
this.cache.set(adapterId, info);
|
|
this.myAdapters.set(adapterId, { info, data: adapterData });
|
|
await this._saveLocalStorage();
|
|
|
|
this.emit('update:complete', info);
|
|
return info;
|
|
}
|
|
|
|
/**
|
|
* Delete an uploaded adapter
|
|
*
|
|
* @param {string} adapterId - Adapter to delete
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async delete(adapterId) {
|
|
if (!this.myAdapters.has(adapterId)) {
|
|
throw new Error(`Adapter not found in your uploads: ${adapterId}`);
|
|
}
|
|
|
|
this.cache.delete(adapterId);
|
|
this.myAdapters.delete(adapterId);
|
|
this.downloaded.delete(adapterId);
|
|
this.reviews.delete(adapterId);
|
|
|
|
await this._saveLocalStorage();
|
|
this.emit('delete:complete', { adapterId });
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get user's uploaded adapters
|
|
*
|
|
* @returns {AdapterInfo[]}
|
|
*/
|
|
getMyAdapters() {
|
|
return Array.from(this.myAdapters.values()).map(a => a.info);
|
|
}
|
|
|
|
// ============================================
|
|
// RATINGS AND REVIEWS
|
|
// ============================================
|
|
|
|
/**
|
|
* Rate an adapter
|
|
*
|
|
* @param {string} adapterId - Adapter to rate
|
|
* @param {number} rating - Rating (1-5)
|
|
* @returns {Promise<void>}
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* await hub.rate('code-assistant-v2', 5);
|
|
* ```
|
|
*/
|
|
async rate(adapterId, rating) {
|
|
if (rating < 1 || rating > 5) {
|
|
throw new Error('Rating must be between 1 and 5');
|
|
}
|
|
|
|
const info = this.cache.get(adapterId);
|
|
if (!info) {
|
|
throw new Error(`Adapter not found: ${adapterId}`);
|
|
}
|
|
|
|
// Update rating (running average)
|
|
const totalRating = (info.rating || 0) * (info.ratingCount || 0);
|
|
info.ratingCount = (info.ratingCount || 0) + 1;
|
|
info.rating = (totalRating + rating) / info.ratingCount;
|
|
|
|
this.cache.set(adapterId, info);
|
|
await this._saveLocalStorage();
|
|
|
|
this.emit('rating:added', { adapterId, rating, newRating: info.rating });
|
|
}
|
|
|
|
/**
|
|
* Add a review for an adapter
|
|
*
|
|
* @param {string} adapterId - Adapter to review
|
|
* @param {number} rating - Rating (1-5)
|
|
* @param {string} comment - Review comment
|
|
* @returns {Promise<Review>}
|
|
*
|
|
* @example
|
|
* ```javascript
|
|
* const review = await hub.review('code-assistant-v2', 5, 'Great for Python!');
|
|
* ```
|
|
*/
|
|
async review(adapterId, rating, comment) {
|
|
// Add rating first
|
|
await this.rate(adapterId, rating);
|
|
|
|
// Create review
|
|
const reviewData = {
|
|
id: `review-${randomBytes(6).toString('hex')}`,
|
|
adapterId,
|
|
authorId: this.userId,
|
|
authorName: `User-${this.userId.slice(0, 8)}`,
|
|
rating,
|
|
comment,
|
|
createdAt: Date.now(),
|
|
helpful: 0,
|
|
};
|
|
|
|
// Store review
|
|
if (!this.reviews.has(adapterId)) {
|
|
this.reviews.set(adapterId, []);
|
|
}
|
|
this.reviews.get(adapterId).push(reviewData);
|
|
|
|
this.emit('review:added', reviewData);
|
|
return reviewData;
|
|
}
|
|
|
|
/**
|
|
* Get reviews for an adapter
|
|
*
|
|
* @param {string} adapterId - Adapter ID
|
|
* @param {Object} [options={}] - Options
|
|
* @returns {Promise<Review[]>}
|
|
*/
|
|
async getReviews(adapterId, options = {}) {
|
|
const { sort = 'recent', limit = 20 } = options;
|
|
|
|
const adapterReviews = this.reviews.get(adapterId) || [];
|
|
|
|
// Sort
|
|
const sorted = [...adapterReviews];
|
|
switch (sort) {
|
|
case 'helpful':
|
|
sorted.sort((a, b) => (b.helpful || 0) - (a.helpful || 0));
|
|
break;
|
|
case 'rating':
|
|
sorted.sort((a, b) => b.rating - a.rating);
|
|
break;
|
|
case 'recent':
|
|
default:
|
|
sorted.sort((a, b) => b.createdAt - a.createdAt);
|
|
}
|
|
|
|
return sorted.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* Mark a review as helpful
|
|
*
|
|
* @param {string} reviewId - Review ID
|
|
*/
|
|
async markHelpful(reviewId) {
|
|
for (const reviews of this.reviews.values()) {
|
|
const review = reviews.find(r => r.id === reviewId);
|
|
if (review) {
|
|
review.helpful = (review.helpful || 0) + 1;
|
|
this.emit('review:helpful', { reviewId, helpful: review.helpful });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// COLLECTIONS AND FAVORITES
|
|
// ============================================
|
|
|
|
/**
|
|
* Create a collection of adapters
|
|
*
|
|
* @param {string} name - Collection name
|
|
* @param {string} [description=''] - Collection description
|
|
* @returns {Object} Collection info
|
|
*/
|
|
createCollection(name, description = '') {
|
|
const id = `collection-${randomBytes(6).toString('hex')}`;
|
|
const collection = {
|
|
id,
|
|
name,
|
|
description,
|
|
adapters: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
};
|
|
|
|
this.emit('collection:created', collection);
|
|
return collection;
|
|
}
|
|
|
|
/**
|
|
* Add adapter to favorites
|
|
*
|
|
* @param {string} adapterId - Adapter to favorite
|
|
*/
|
|
addFavorite(adapterId) {
|
|
this.emit('favorite:added', { adapterId });
|
|
}
|
|
|
|
/**
|
|
* Remove adapter from favorites
|
|
*
|
|
* @param {string} adapterId - Adapter to unfavorite
|
|
*/
|
|
removeFavorite(adapterId) {
|
|
this.emit('favorite:removed', { adapterId });
|
|
}
|
|
|
|
// ============================================
|
|
// UTILITY METHODS
|
|
// ============================================
|
|
|
|
/**
|
|
* Get detailed info about an adapter
|
|
*
|
|
* @param {string} adapterId - Adapter ID
|
|
* @returns {Promise<AdapterInfo|null>}
|
|
*/
|
|
async getAdapterInfo(adapterId) {
|
|
return this.cache.get(adapterId) || null;
|
|
}
|
|
|
|
/**
|
|
* Check if an adapter exists
|
|
*
|
|
* @param {string} adapterId - Adapter ID
|
|
* @returns {boolean}
|
|
*/
|
|
exists(adapterId) {
|
|
return this.cache.has(adapterId);
|
|
}
|
|
|
|
/**
|
|
* Get hub statistics
|
|
*
|
|
* @returns {Object}
|
|
*/
|
|
getStats() {
|
|
return {
|
|
...this.stats,
|
|
totalAdapters: this.cache.size,
|
|
downloadedAdapters: this.downloaded.size,
|
|
myAdapters: this.myAdapters.size,
|
|
totalReviews: Array.from(this.reviews.values()).reduce((sum, r) => sum + r.length, 0),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear all caches
|
|
*/
|
|
async clearCache() {
|
|
this.downloaded.clear();
|
|
this.emit('cache:cleared');
|
|
}
|
|
|
|
/**
|
|
* Seed hub with sample adapters for demo purposes
|
|
*
|
|
* @param {number} [count=20] - Number of sample adapters
|
|
*/
|
|
async seedSampleAdapters(count = 20) {
|
|
const domains = Object.keys(ADAPTER_DOMAINS);
|
|
const models = ['phi-1.5-int4', 'distilgpt2', 'gpt2', 'starcoder-tiny'];
|
|
const adjectives = ['Advanced', 'Pro', 'Ultra', 'Smart', 'Fast', 'Accurate', 'Helpful'];
|
|
const nouns = ['Assistant', 'Helper', 'Expert', 'Companion', 'Generator', 'Wizard'];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const domain = domains[Math.floor(Math.random() * domains.length)];
|
|
const model = models[Math.floor(Math.random() * models.length)];
|
|
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
const name = `${adj} ${ADAPTER_DOMAINS[domain].name.split(' ')[0]} ${noun}`;
|
|
|
|
const info = {
|
|
id: `sample-${domain}-${randomBytes(4).toString('hex')}`,
|
|
name,
|
|
description: `A ${adj.toLowerCase()} adapter for ${ADAPTER_DOMAINS[domain].description.toLowerCase()}`,
|
|
author: `sample-author-${i % 5}`,
|
|
authorId: `sample-author-${i % 5}`,
|
|
baseModel: model,
|
|
domain,
|
|
tags: [domain, ...ADAPTER_DOMAINS[domain].subdomains.slice(0, 2)],
|
|
rating: 3 + Math.random() * 2,
|
|
ratingCount: Math.floor(Math.random() * 100) + 1,
|
|
downloads: Math.floor(Math.random() * 10000),
|
|
size: Math.floor(Math.random() * 50000) + 10000,
|
|
version: `1.${Math.floor(Math.random() * 10)}.0`,
|
|
license: 'MIT',
|
|
createdAt: Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000),
|
|
updatedAt: Date.now() - Math.floor(Math.random() * 7 * 24 * 60 * 60 * 1000),
|
|
config: {
|
|
rank: [4, 8, 16][Math.floor(Math.random() * 3)],
|
|
alpha: [8, 16, 32][Math.floor(Math.random() * 3)],
|
|
embeddingDim: 384,
|
|
},
|
|
stats: {
|
|
trainingSamples: Math.floor(Math.random() * 10000) + 100,
|
|
trainingEpochs: Math.floor(Math.random() * 50) + 5,
|
|
},
|
|
};
|
|
|
|
this.cache.set(info.id, info);
|
|
}
|
|
|
|
await this._saveLocalStorage();
|
|
this.emit('seed:complete', { count });
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// EXPORTS
|
|
// ============================================
|
|
|
|
export default AdapterHub;
|