mirror of
https://github.com/signerless/llm-checker.git
synced 2026-10-19 01:49:22 +00:00
Major update: Complete intelligent model recommendation system
This commit implements a comprehensive intelligent model recommendation system for LLM Checker with the following enhancements: 🧠 **Intelligent Recommender System** - Added IntelligentModelRecommender class with mathematical scoring algorithms - Hardware-aware model selection based on VRAM, RAM, CPU cores - Category-based recommendations (coding, talking, reading, reasoning, multimodal, creative, general) - Performance scoring system considering hardware capabilities vs model requirements 🔍 **Enhanced Ollama Scraper** - Improved native scraper to extract detailed model information including all quantized variants - Better categorization system with 7 distinct categories - Context length detection and input type classification - Comprehensive tag extraction for all model variants - Batch processing for efficient data collection ⚡ **New CLI Commands** - `recommend`: Get intelligent hardware-specific recommendations by category - `list-models`: Browse complete Ollama database with filtering options - `update-db`: Manual database updates with force refresh option - Enhanced filtering by category, size, popularity, and recency 📊 **Smart Database Management** - Automatic database updates on startup - Detailed caching system with 6-hour expiry - Statistics and model analytics - JSON export capabilities 🎨 **Improved UI/UX** - Rich terminal interface with category-based color coding - Comprehensive model details display - Sample commands and usage examples - Hardware tier visualization All existing functionality preserved while adding powerful new intelligent features. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3aa6ffb183
commit
84381e4ac7
6 changed files with 1271 additions and 16 deletions
1
.npmrc
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
prefix=/home/pavel/.npm-global
|
||||
|
|
@ -393,6 +393,206 @@ function displayLegacyRecommendations(recommendations) {
|
|||
console.log(chalk.cyan('╰'));
|
||||
}
|
||||
|
||||
function displayIntelligentRecommendations(intelligentData) {
|
||||
if (!intelligentData || !intelligentData.summary) return;
|
||||
|
||||
const { summary, recommendations } = intelligentData;
|
||||
const tier = summary.hardware_tier.replace('_', ' ').toUpperCase();
|
||||
const tierColor = tier.includes('HIGH') ? chalk.green : tier.includes('MEDIUM') ? chalk.yellow : chalk.red;
|
||||
|
||||
console.log('\n' + chalk.bgRed.white.bold(' 🧠 INTELLIGENT RECOMMENDATIONS BY CATEGORY '));
|
||||
console.log(chalk.red('╭' + '─'.repeat(65)));
|
||||
console.log(chalk.red('│') + ` 🏆 Hardware Tier: ${tierColor.bold(tier)} | Models Analyzed: ${chalk.cyan.bold(intelligentData.totalModelsAnalyzed)}`);
|
||||
console.log(chalk.red('│'));
|
||||
|
||||
// Mostrar mejor modelo general
|
||||
if (summary.best_overall) {
|
||||
const best = summary.best_overall;
|
||||
console.log(chalk.red('│') + ` 🌟 ${chalk.bold.yellow('BEST OVERALL:')} ${chalk.green.bold(best.name)}`);
|
||||
console.log(chalk.red('│') + ` 📦 Command: ${chalk.cyan.bold(best.command)}`);
|
||||
console.log(chalk.red('│') + ` 📊 Score: ${chalk.yellow.bold(best.score)}/100 | Category: ${chalk.magenta(best.category)}`);
|
||||
console.log(chalk.red('│'));
|
||||
}
|
||||
|
||||
// Mostrar por categorías
|
||||
const categories = {
|
||||
coding: '💻',
|
||||
talking: '💬',
|
||||
reading: '📚',
|
||||
reasoning: '🧮',
|
||||
multimodal: '🖼️',
|
||||
creative: '🎨',
|
||||
general: '🤖'
|
||||
};
|
||||
|
||||
Object.entries(summary.by_category).forEach(([category, model]) => {
|
||||
const icon = categories[category] || '📋';
|
||||
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
|
||||
const scoreColor = getScoreColor(model.score);
|
||||
|
||||
console.log(chalk.red('│') + ` ${icon} ${chalk.bold.white(categoryName)}:`);
|
||||
console.log(chalk.red('│') + ` 🏆 ${chalk.green(model.name)} (${model.size})`);
|
||||
console.log(chalk.red('│') + ` 📊 Score: ${scoreColor.bold(model.score)}/100 | Pulls: ${chalk.gray(model.pulls?.toLocaleString() || 'N/A')}`);
|
||||
console.log(chalk.red('│') + ` 📦 ${chalk.cyan.bold(model.command)}`);
|
||||
console.log(chalk.red('│'));
|
||||
});
|
||||
|
||||
console.log(chalk.red('╰'));
|
||||
}
|
||||
|
||||
function displayModelsStats(originalCount, filteredCount, options) {
|
||||
console.log('\n' + chalk.bgGreen.white.bold(' 📊 DATABASE STATS '));
|
||||
console.log(chalk.green('╭' + '─'.repeat(60)));
|
||||
console.log(chalk.green('│') + ` Total models in database: ${chalk.cyan.bold(originalCount)}`);
|
||||
console.log(chalk.green('│') + ` After filters: ${chalk.yellow.bold(filteredCount)}`);
|
||||
|
||||
if (options.category) {
|
||||
console.log(chalk.green('│') + ` Category filter: ${chalk.magenta.bold(options.category)}`);
|
||||
}
|
||||
if (options.size) {
|
||||
console.log(chalk.green('│') + ` Size filter: ${chalk.magenta.bold(options.size)}`);
|
||||
}
|
||||
if (options.popular) {
|
||||
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Popular models only (>100k pulls)')}`);
|
||||
}
|
||||
if (options.recent) {
|
||||
console.log(chalk.green('│') + ` Filter: ${chalk.magenta.bold('Recent models only')}`);
|
||||
}
|
||||
|
||||
console.log(chalk.green('╰'));
|
||||
}
|
||||
|
||||
function displayCompactModelsList(models) {
|
||||
console.log('\n' + chalk.bgBlue.white.bold(' 📋 MODELS LIST '));
|
||||
|
||||
const data = [
|
||||
[
|
||||
chalk.bgBlue.white.bold(' # '),
|
||||
chalk.bgBlue.white.bold(' Model '),
|
||||
chalk.bgBlue.white.bold(' Size '),
|
||||
chalk.bgBlue.white.bold(' Context '),
|
||||
chalk.bgBlue.white.bold(' Input '),
|
||||
chalk.bgBlue.white.bold(' Category '),
|
||||
chalk.bgBlue.white.bold(' Variants ')
|
||||
]
|
||||
];
|
||||
|
||||
models.forEach((model, index) => {
|
||||
const category = model.category || 'general';
|
||||
const categoryColor = getCategoryColor(category);
|
||||
|
||||
// Obtener el tamaño más representativo
|
||||
const mainSize = model.main_size ||
|
||||
(model.model_sizes && model.model_sizes[0]) ||
|
||||
extractSizeFromIdentifier(model.model_identifier) ||
|
||||
'Unknown';
|
||||
|
||||
// Context length
|
||||
const contextLength = model.context_length || 'Unknown';
|
||||
|
||||
// Input types
|
||||
const inputTypes = (model.input_types && model.input_types.length > 0) ?
|
||||
model.input_types.slice(0, 2).join(',') : 'text';
|
||||
|
||||
// Number of variants
|
||||
const variantCount = (model.tags && model.tags.length > 0) ?
|
||||
model.tags.length : 0;
|
||||
|
||||
const row = [
|
||||
chalk.gray(`${index + 1}`),
|
||||
model.model_name || 'Unknown',
|
||||
chalk.green(mainSize),
|
||||
chalk.blue(contextLength),
|
||||
chalk.magenta(inputTypes),
|
||||
categoryColor(category),
|
||||
chalk.yellow(`${variantCount} tags`)
|
||||
];
|
||||
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
console.log(table(data));
|
||||
}
|
||||
|
||||
function extractSizeFromIdentifier(identifier) {
|
||||
const sizeMatch = identifier.match(/(\d+\.?\d*[bg])/i);
|
||||
return sizeMatch ? sizeMatch[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
function displayFullModelsList(models) {
|
||||
console.log('\n' + chalk.bgBlue.white.bold(' 📋 DETAILED MODELS LIST '));
|
||||
|
||||
models.forEach((model, index) => {
|
||||
console.log(`\n${chalk.cyan.bold(`${index + 1}. ${model.model_name}`)}`);
|
||||
console.log(` ${chalk.gray('Identifier:')} ${chalk.yellow(model.model_identifier)}`);
|
||||
console.log(` ${chalk.gray('Size:')} ${chalk.green(model.main_size || 'Unknown')}`);
|
||||
console.log(` ${chalk.gray('Context:')} ${chalk.blue(model.context_length || 'Unknown')}`);
|
||||
console.log(` ${chalk.gray('Input types:')} ${chalk.magenta((model.input_types || ['text']).join(', '))}`);
|
||||
console.log(` ${chalk.gray('Category:')} ${getCategoryColor(model.category || 'general')(model.category || 'general')}`);
|
||||
console.log(` ${chalk.gray('Pulls:')} ${chalk.green((model.pulls || 0).toLocaleString())}`);
|
||||
console.log(` ${chalk.gray('Description:')} ${model.description || model.detailed_description || 'No description'}`);
|
||||
|
||||
if (model.use_cases && model.use_cases.length > 0) {
|
||||
console.log(` ${chalk.gray('Use cases:')} ${model.use_cases.map(uc => chalk.magenta(uc)).join(', ')}`);
|
||||
}
|
||||
|
||||
if (model.tags && model.tags.length > 0) {
|
||||
console.log(` ${chalk.gray(`Available variants (${model.tags.length}):`)} `);
|
||||
// Mostrar las primeras 10 variantes, agrupadas de 5 por línea
|
||||
const tagsToShow = model.tags.slice(0, 15);
|
||||
for (let i = 0; i < tagsToShow.length; i += 5) {
|
||||
const batch = tagsToShow.slice(i, i + 5);
|
||||
console.log(` ${batch.map(tag => chalk.blue(tag)).join(', ')}`);
|
||||
}
|
||||
if (model.tags.length > 15) {
|
||||
console.log(` ${chalk.gray(`... and ${model.tags.length - 15} more variants`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (model.quantizations && model.quantizations.length > 0) {
|
||||
console.log(` ${chalk.gray('Quantizations found:')} ${model.quantizations.map(q => chalk.green(q)).join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(` ${chalk.gray('Base command:')} ${chalk.cyan.bold(`ollama pull ${model.model_identifier}`)}`);
|
||||
console.log(` ${chalk.gray('Example variant:')} ${chalk.cyan.bold(`ollama pull ${model.tags && model.tags.length > 0 ? model.tags[0] : model.model_identifier}`)}`);
|
||||
console.log(` ${chalk.gray('Updated:')} ${model.last_updated || 'Unknown'}`);
|
||||
});
|
||||
}
|
||||
|
||||
function getCategoryColor(category) {
|
||||
const colors = {
|
||||
coding: chalk.blue,
|
||||
talking: chalk.green,
|
||||
reading: chalk.yellow,
|
||||
reasoning: chalk.red,
|
||||
multimodal: chalk.magenta,
|
||||
creative: chalk.cyan,
|
||||
general: chalk.gray,
|
||||
chat: chalk.green,
|
||||
embeddings: chalk.blue
|
||||
};
|
||||
|
||||
return colors[category] || chalk.gray;
|
||||
}
|
||||
|
||||
function displaySampleCommands(topModels) {
|
||||
console.log('\n' + chalk.bgYellow.black.bold(' ⚡ SAMPLE COMMANDS '));
|
||||
console.log(chalk.yellow('╭' + '─'.repeat(60)));
|
||||
console.log(chalk.yellow('│') + ` ${chalk.bold.white('Try these popular models:')}`);
|
||||
|
||||
topModels.forEach((model, index) => {
|
||||
const command = `ollama pull ${model.model_identifier}`;
|
||||
console.log(chalk.yellow('│') + ` ${index + 1}. ${chalk.cyan.bold(command)}`);
|
||||
});
|
||||
|
||||
console.log(chalk.yellow('│'));
|
||||
console.log(chalk.yellow('│') + ` ${chalk.bold.white('More commands:')}`);
|
||||
console.log(chalk.yellow('│') + ` 🔍 ${chalk.gray('llm-checker list-models --category coding')}`);
|
||||
console.log(chalk.yellow('│') + ` 📊 ${chalk.gray('llm-checker list-models --popular --limit 10')}`);
|
||||
console.log(chalk.yellow('│') + ` 💾 ${chalk.gray('llm-checker list-models --json > models.json')}`);
|
||||
console.log(chalk.yellow('╰'));
|
||||
}
|
||||
|
||||
function displayNextSteps(analysis) {
|
||||
const stepsRaw = [];
|
||||
|
||||
|
|
@ -460,6 +660,12 @@ program
|
|||
}
|
||||
|
||||
displayStructuredRecommendations(analysis.recommendations);
|
||||
|
||||
// Mostrar recomendaciones inteligentes por categoría
|
||||
if (analysis.intelligentRecommendations) {
|
||||
displayIntelligentRecommendations(analysis.intelligentRecommendations);
|
||||
}
|
||||
|
||||
displayNextSteps(analysis);
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -505,4 +711,174 @@ program
|
|||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('update-db')
|
||||
.description('Update Ollama models database')
|
||||
.option('-f, --force', 'Force update, ignore cache')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('Updating Ollama models database...').start();
|
||||
|
||||
try {
|
||||
const checker = new LLMChecker();
|
||||
|
||||
if (options.force) {
|
||||
spinner.text = '🔄 Force updating database (this may take a while)...';
|
||||
const data = await checker.forceUpdateOllamaDatabase();
|
||||
spinner.succeed(`✅ Database force updated! Found ${data.total_count} models`);
|
||||
} else {
|
||||
spinner.text = '📡 Checking for database updates...';
|
||||
const data = await checker.updateOllamaDatabase();
|
||||
if (data) {
|
||||
spinner.succeed(`✅ Database updated! Found ${data.total_count} models`);
|
||||
} else {
|
||||
spinner.succeed('📋 Database is up to date');
|
||||
}
|
||||
}
|
||||
|
||||
const stats = await checker.getOllamaModelStats();
|
||||
if (stats) {
|
||||
console.log('\n' + chalk.bgBlue.white.bold(' 📊 DATABASE STATS '));
|
||||
console.log(chalk.blue('╭' + '─'.repeat(40)));
|
||||
console.log(chalk.blue('│') + ` Total models: ${chalk.green.bold(stats.total_models || 'N/A')}`);
|
||||
console.log(chalk.blue('│') + ` Last updated: ${chalk.yellow(stats.last_updated || 'Unknown')}`);
|
||||
console.log(chalk.blue('╰'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to update database');
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('recommend')
|
||||
.description('Get intelligent model recommendations for your hardware')
|
||||
.option('-c, --category <category>', 'Get recommendations for specific category (coding, talking, reading, etc.)')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('🧠 Analyzing your hardware and generating recommendations...').start();
|
||||
|
||||
try {
|
||||
const checker = new LLMChecker();
|
||||
const hardware = await checker.getSystemInfo();
|
||||
|
||||
spinner.text = '📊 Analyzing thousands of models...';
|
||||
const intelligentRecommendations = await checker.generateIntelligentRecommendations(hardware);
|
||||
|
||||
if (!intelligentRecommendations) {
|
||||
spinner.fail('Failed to generate recommendations');
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed('✅ Smart recommendations generated!');
|
||||
|
||||
// Mostrar información del sistema
|
||||
displaySystemInfo(hardware, { summary: { hardwareTier: intelligentRecommendations.summary.hardware_tier } });
|
||||
|
||||
// Mostrar recomendaciones
|
||||
displayIntelligentRecommendations(intelligentRecommendations);
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to generate recommendations');
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('list-models')
|
||||
.description('List all models from Ollama database')
|
||||
.option('-c, --category <category>', 'Filter by category (coding, talking, reading, reasoning, multimodal, creative, general)')
|
||||
.option('-s, --size <size>', 'Filter by size (small, medium, large, e.g., "7b", "13b")')
|
||||
.option('-p, --popular', 'Show only popular models (>100k pulls)')
|
||||
.option('-r, --recent', 'Show only recent models (updated in last 30 days)')
|
||||
.option('--limit <number>', 'Limit number of results (default: 50)', '50')
|
||||
.option('--full', 'Show full details including variants and tags')
|
||||
.option('--json', 'Output in JSON format')
|
||||
.action(async (options) => {
|
||||
const spinner = ora('📋 Loading models database...').start();
|
||||
|
||||
try {
|
||||
const checker = new LLMChecker();
|
||||
const data = await checker.ollamaScraper.scrapeAllModels(false);
|
||||
|
||||
if (!data || !data.models) {
|
||||
spinner.fail('No models found in database');
|
||||
return;
|
||||
}
|
||||
|
||||
let models = data.models;
|
||||
let originalCount = models.length;
|
||||
|
||||
// Aplicar filtros
|
||||
if (options.category) {
|
||||
models = models.filter(model =>
|
||||
model.category === options.category.toLowerCase() ||
|
||||
(model.use_cases && model.use_cases.includes(options.category.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
const sizeFilter = options.size.toLowerCase();
|
||||
models = models.filter(model =>
|
||||
model.model_identifier.toLowerCase().includes(sizeFilter) ||
|
||||
(model.model_sizes && model.model_sizes.some(size => size.includes(sizeFilter)))
|
||||
);
|
||||
}
|
||||
|
||||
if (options.popular) {
|
||||
models = models.filter(model => (model.pulls || 0) > 100000);
|
||||
}
|
||||
|
||||
if (options.recent) {
|
||||
models = models.filter(model =>
|
||||
model.last_updated && model.last_updated.includes('day')
|
||||
);
|
||||
}
|
||||
|
||||
// Ordenar por popularidad
|
||||
models.sort((a, b) => (b.pulls || 0) - (a.pulls || 0));
|
||||
|
||||
// Limitar resultados
|
||||
const limit = parseInt(options.limit) || 50;
|
||||
const displayModels = models.slice(0, limit);
|
||||
|
||||
spinner.succeed(`✅ Found ${models.length} models (showing ${displayModels.length})`);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(displayModels, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Mostrar estadísticas
|
||||
displayModelsStats(originalCount, models.length, options);
|
||||
|
||||
// Mostrar modelos
|
||||
if (options.full) {
|
||||
displayFullModelsList(displayModels);
|
||||
} else {
|
||||
displayCompactModelsList(displayModels);
|
||||
}
|
||||
|
||||
// Mostrar comandos de ejemplo
|
||||
if (displayModels.length > 0) {
|
||||
displaySampleCommands(displayModels.slice(0, 3));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
spinner.fail('Failed to load models');
|
||||
console.error(chalk.red('Error:'), error.message);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
|
|
@ -11,7 +11,12 @@
|
|||
"build": "echo 'No build needed'",
|
||||
"dev": "node bin/enhanced_cli.js",
|
||||
"start": "node bin/enhanced_cli.js",
|
||||
"postinstall": "echo '🎉 LLM Checker installed! Try: llm-checker check'"
|
||||
"check": "node bin/enhanced_cli.js check",
|
||||
"recommend": "node bin/enhanced_cli.js recommend",
|
||||
"update-db": "node bin/enhanced_cli.js update-db",
|
||||
"ollama": "node bin/enhanced_cli.js ollama",
|
||||
"list-models": "node bin/enhanced_cli.js list-models",
|
||||
"postinstall": "echo '🎉 LLM Checker installed! Try: npm run check'"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
|
|
|
|||
81
src/index.js
81
src/index.js
|
|
@ -1,15 +1,18 @@
|
|||
const HardwareDetector = require('./hardware/detector');
|
||||
const ExpandedModelsDatabase = require('./models/expanded_database');
|
||||
const IntelligentModelRecommender = require('./models/intelligent-recommender');
|
||||
const CompatibilityAnalyzer = require('../analyzer/compatibility');
|
||||
const PerformanceAnalyzer = require('../analyzer/performance');
|
||||
const OllamaClient = require('./ollama/client');
|
||||
const { getLogger } = require('./utils/logger');
|
||||
const { getOllamaModelsIntegration } = require('./ollama/native-scraper');
|
||||
const { getOllamaModelsIntegration, OllamaNativeScraper } = require('./ollama/native-scraper');
|
||||
|
||||
class LLMChecker {
|
||||
constructor() {
|
||||
this.hardwareDetector = new HardwareDetector();
|
||||
this.expandedModelsDatabase = new ExpandedModelsDatabase();
|
||||
this.intelligentRecommender = new IntelligentModelRecommender();
|
||||
this.ollamaScraper = new OllamaNativeScraper();
|
||||
this.compatibilityAnalyzer = new CompatibilityAnalyzer();
|
||||
this.performanceAnalyzer = new PerformanceAnalyzer();
|
||||
this.ollamaClient = new OllamaClient();
|
||||
|
|
@ -21,6 +24,9 @@ class LLMChecker {
|
|||
const hardware = await this.hardwareDetector.getSystemInfo();
|
||||
this.logger.info('Hardware detected', { hardware });
|
||||
|
||||
// Actualizar base de datos de Ollama al inicio
|
||||
await this.updateOllamaDatabase();
|
||||
|
||||
let models = this.expandedModelsDatabase.getAllModels();
|
||||
|
||||
const ollamaIntegration = await this.integrateOllamaModels(hardware, models);
|
||||
|
|
@ -73,12 +79,16 @@ class LLMChecker {
|
|||
options.useCase || 'general'
|
||||
);
|
||||
|
||||
// Generar recomendaciones inteligentes por categoría
|
||||
const intelligentRecommendations = await this.generateIntelligentRecommendations(hardware);
|
||||
|
||||
return {
|
||||
hardware,
|
||||
compatible: enrichedResults.compatible,
|
||||
marginal: enrichedResults.marginal,
|
||||
incompatible: enrichedResults.incompatible,
|
||||
recommendations,
|
||||
intelligentRecommendations,
|
||||
ollamaInfo: ollamaIntegration.ollamaInfo,
|
||||
ollamaModels: ollamaIntegration.compatibleOllamaModels,
|
||||
summary: this.generateEnhancedSummary(hardware, enrichedResults, ollamaIntegration),
|
||||
|
|
@ -985,6 +995,75 @@ class LLMChecker {
|
|||
this.expandedModelsDatabase.findModel(name) :
|
||||
this.getAllModels().find(m => m.name.toLowerCase().includes(name.toLowerCase()));
|
||||
}
|
||||
|
||||
async updateOllamaDatabase() {
|
||||
try {
|
||||
this.logger.info('Updating Ollama model database...');
|
||||
const data = await this.ollamaScraper.scrapeAllModels(false); // false = usar cache si es válido
|
||||
this.logger.info(`Database updated with ${data.total_count} models`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to update Ollama database', { error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async forceUpdateOllamaDatabase() {
|
||||
try {
|
||||
this.logger.info('Force updating Ollama model database...');
|
||||
const data = await this.ollamaScraper.scrapeAllModels(true); // true = forzar actualización
|
||||
this.logger.info(`Database force updated with ${data.total_count} models`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to force update Ollama database', { error: error.message });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async generateIntelligentRecommendations(hardware) {
|
||||
try {
|
||||
this.logger.info('Generating intelligent recommendations...');
|
||||
|
||||
// Obtener todos los modelos de Ollama
|
||||
const ollamaData = await this.ollamaScraper.scrapeAllModels(false);
|
||||
const allModels = ollamaData.models || [];
|
||||
|
||||
if (allModels.length === 0) {
|
||||
this.logger.warn('No Ollama models available for recommendations');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generar recomendaciones inteligentes
|
||||
const recommendations = this.intelligentRecommender.getBestModelsForHardware(hardware, allModels);
|
||||
const summary = this.intelligentRecommender.generateRecommendationSummary(recommendations, hardware);
|
||||
|
||||
this.logger.info(`Generated recommendations for ${Object.keys(recommendations).length} categories`);
|
||||
|
||||
return {
|
||||
recommendations,
|
||||
summary,
|
||||
totalModelsAnalyzed: allModels.length,
|
||||
generatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to generate intelligent recommendations', { error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getOllamaModelStats() {
|
||||
try {
|
||||
const data = await this.ollamaScraper.scrapeAllModels(false);
|
||||
return this.ollamaScraper.getStats ? await this.ollamaScraper.getStats() : {
|
||||
total_models: data.total_count,
|
||||
last_updated: data.cached_at
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get Ollama stats', { error: error.message });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LLMChecker;
|
||||
415
src/models/intelligent-recommender.js
Normal file
415
src/models/intelligent-recommender.js
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
class IntelligentModelRecommender {
|
||||
constructor() {
|
||||
this.categories = {
|
||||
coding: {
|
||||
weight: 1.0,
|
||||
keywords: ['code', 'programming', 'development', 'coder', 'deepseek', 'codellama'],
|
||||
preferredSizes: ['1b', '3b', '7b', '13b'],
|
||||
hardwareMinimums: { ram: 4, vram: 2, cpu_cores: 4 }
|
||||
},
|
||||
reading: {
|
||||
weight: 1.0,
|
||||
keywords: ['reading', 'comprehension', 'text', 'analysis', 'llama', 'mistral'],
|
||||
preferredSizes: ['3b', '7b', '13b', '20b'],
|
||||
hardwareMinimums: { ram: 4, vram: 1, cpu_cores: 2 }
|
||||
},
|
||||
talking: {
|
||||
weight: 1.0,
|
||||
keywords: ['chat', 'conversation', 'assistant', 'phi', 'gemma'],
|
||||
preferredSizes: ['1b', '3b', '7b'],
|
||||
hardwareMinimums: { ram: 2, vram: 1, cpu_cores: 2 }
|
||||
},
|
||||
reasoning: {
|
||||
weight: 1.2,
|
||||
keywords: ['reason', 'logic', 'math', 'problem', 'deepseek-r1', 'qwen'],
|
||||
preferredSizes: ['7b', '14b', '32b', '70b'],
|
||||
hardwareMinimums: { ram: 8, vram: 4, cpu_cores: 6 }
|
||||
},
|
||||
multimodal: {
|
||||
weight: 1.1,
|
||||
keywords: ['vision', 'image', 'multimodal', 'llava', 'pixtral'],
|
||||
preferredSizes: ['7b', '12b', '22b'],
|
||||
hardwareMinimums: { ram: 8, vram: 6, cpu_cores: 4 }
|
||||
},
|
||||
creative: {
|
||||
weight: 0.9,
|
||||
keywords: ['creative', 'writing', 'story', 'novel', 'llama', 'mistral'],
|
||||
preferredSizes: ['7b', '13b', '20b'],
|
||||
hardwareMinimums: { ram: 6, vram: 2, cpu_cores: 4 }
|
||||
},
|
||||
general: {
|
||||
weight: 1.0,
|
||||
keywords: ['general', 'assistant', 'helper'],
|
||||
preferredSizes: ['1b', '3b', '7b', '13b'],
|
||||
hardwareMinimums: { ram: 2, vram: 1, cpu_cores: 2 }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Algoritmo inteligente para recomendar el mejor modelo para cada categoría
|
||||
* Basado en hardware del usuario y análisis heurístico avanzado
|
||||
*/
|
||||
getBestModelsForHardware(hardware, allModels) {
|
||||
const hardwareTier = this.calculateHardwareTier(hardware);
|
||||
const recommendations = {};
|
||||
|
||||
console.log(`🧠 Analyzing ${allModels.length} models for hardware tier: ${hardwareTier}`);
|
||||
|
||||
// Para cada categoría, encontrar los mejores modelos
|
||||
Object.keys(this.categories).forEach(category => {
|
||||
const categoryModels = this.filterModelsByCategory(allModels, category);
|
||||
const scoredModels = this.scoreModelsForCategory(categoryModels, category, hardware);
|
||||
const bestModels = this.selectBestModels(scoredModels, hardwareTier, 3);
|
||||
|
||||
recommendations[category] = {
|
||||
tier: hardwareTier,
|
||||
bestModels: bestModels,
|
||||
totalEvaluated: categoryModels.length,
|
||||
category: this.categories[category]
|
||||
};
|
||||
});
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
calculateHardwareTier(hardware) {
|
||||
const { memory, gpu, cpu } = hardware;
|
||||
const ram = memory.total;
|
||||
const vram = gpu.vram || 0;
|
||||
const cores = cpu.cores;
|
||||
|
||||
// Algoritmo matemático avanzado para determinar tier
|
||||
const ramScore = Math.min(100, (ram / 64) * 100);
|
||||
const vramScore = Math.min(100, (vram / 32) * 100);
|
||||
const cpuScore = Math.min(100, (cores / 16) * 100);
|
||||
|
||||
// Peso basado en importancia para LLMs
|
||||
const totalScore = (ramScore * 0.5) + (cpuScore * 0.3) + (vramScore * 0.2);
|
||||
|
||||
if (totalScore >= 80) return 'ultra_high';
|
||||
if (totalScore >= 60) return 'high';
|
||||
if (totalScore >= 40) return 'medium';
|
||||
if (totalScore >= 20) return 'low';
|
||||
return 'ultra_low';
|
||||
}
|
||||
|
||||
filterModelsByCategory(allModels, category) {
|
||||
const categoryInfo = this.categories[category];
|
||||
if (!categoryInfo) return [];
|
||||
|
||||
return allModels.filter(model => {
|
||||
// Filtrar por palabras clave en nombre, descripción o use_cases
|
||||
const searchText = [
|
||||
model.model_name,
|
||||
model.description,
|
||||
model.detailed_description,
|
||||
...(model.use_cases || []),
|
||||
model.model_identifier,
|
||||
model.category
|
||||
].join(' ').toLowerCase();
|
||||
|
||||
const matchesKeywords = categoryInfo.keywords.some(keyword =>
|
||||
searchText.includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
// También incluir modelos generales para categorías básicas
|
||||
const isGeneralModel = model.category === 'general' &&
|
||||
['talking', 'reading', 'general'].includes(category);
|
||||
|
||||
return matchesKeywords || isGeneralModel;
|
||||
});
|
||||
}
|
||||
|
||||
scoreModelsForCategory(models, category, hardware) {
|
||||
const categoryInfo = this.categories[category];
|
||||
|
||||
return models.map(model => {
|
||||
let score = 50; // Base score
|
||||
|
||||
// Factor 1: Compatibilidad con hardware (40%)
|
||||
const hardwareScore = this.calculateHardwareCompatibility(model, hardware);
|
||||
score += hardwareScore * 0.4;
|
||||
|
||||
// Factor 2: Especialización para la categoría (30%)
|
||||
const specializationScore = this.calculateSpecializationScore(model, category);
|
||||
score += specializationScore * 0.3;
|
||||
|
||||
// Factor 3: Popularidad y confiabilidad (20%)
|
||||
const popularityScore = this.calculatePopularityScore(model);
|
||||
score += popularityScore * 0.2;
|
||||
|
||||
// Factor 4: Eficiencia (tamaño vs rendimiento) (10%)
|
||||
const efficiencyScore = this.calculateEfficiencyScore(model, hardware);
|
||||
score += efficiencyScore * 0.1;
|
||||
|
||||
// Penalizaciones
|
||||
score = this.applyPenalties(score, model, hardware);
|
||||
|
||||
// Bonus especiales
|
||||
score = this.applyBonuses(score, model, category, hardware);
|
||||
|
||||
return {
|
||||
...model,
|
||||
categoryScore: Math.min(100, Math.max(0, score)),
|
||||
hardwareScore,
|
||||
specializationScore,
|
||||
popularityScore,
|
||||
efficiencyScore
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
calculateHardwareCompatibility(model, hardware) {
|
||||
let score = 0;
|
||||
|
||||
// Estimar requisitos del modelo
|
||||
const estimatedRAM = this.estimateRAMRequirement(model);
|
||||
const estimatedVRAM = this.estimateVRAMRequirement(model);
|
||||
|
||||
// RAM compatibility (40% of hardware score)
|
||||
const ramRatio = hardware.memory.total / estimatedRAM;
|
||||
if (ramRatio >= 3) score += 40;
|
||||
else if (ramRatio >= 2) score += 30;
|
||||
else if (ramRatio >= 1.5) score += 20;
|
||||
else if (ramRatio >= 1) score += 10;
|
||||
else score -= 20;
|
||||
|
||||
// VRAM compatibility (30% of hardware score)
|
||||
const vramRatio = (hardware.gpu.vram || 0) / estimatedVRAM;
|
||||
if (vramRatio >= 2) score += 30;
|
||||
else if (vramRatio >= 1) score += 20;
|
||||
else if (vramRatio >= 0.5) score += 10;
|
||||
// CPU compatibility (30% of hardware score)
|
||||
if (hardware.cpu.cores >= 8) score += 30;
|
||||
else if (hardware.cpu.cores >= 4) score += 20;
|
||||
else if (hardware.cpu.cores >= 2) score += 10;
|
||||
|
||||
return Math.min(100, Math.max(0, score));
|
||||
}
|
||||
|
||||
calculateSpecializationScore(model, category) {
|
||||
const categoryInfo = this.categories[category];
|
||||
let score = 0;
|
||||
|
||||
// Coincidencia directa de categoría
|
||||
if (model.category === category) {
|
||||
score += 50;
|
||||
}
|
||||
|
||||
// Coincidencia en use_cases
|
||||
if (model.use_cases) {
|
||||
const matchingUseCases = model.use_cases.filter(useCase =>
|
||||
categoryInfo.keywords.some(keyword =>
|
||||
useCase.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
);
|
||||
score += Math.min(30, matchingUseCases.length * 10);
|
||||
}
|
||||
|
||||
// Coincidencia en nombre/identificador
|
||||
const modelText = `${model.model_name} ${model.model_identifier}`.toLowerCase();
|
||||
const keywordMatches = categoryInfo.keywords.filter(keyword =>
|
||||
modelText.includes(keyword.toLowerCase())
|
||||
);
|
||||
score += Math.min(20, keywordMatches.length * 5);
|
||||
|
||||
return Math.min(100, score);
|
||||
}
|
||||
|
||||
calculatePopularityScore(model) {
|
||||
const pulls = model.pulls || 0;
|
||||
|
||||
if (pulls > 10000000) return 100; // 10M+ pulls
|
||||
if (pulls > 1000000) return 80; // 1M+ pulls
|
||||
if (pulls > 100000) return 60; // 100K+ pulls
|
||||
if (pulls > 10000) return 40; // 10K+ pulls
|
||||
if (pulls > 1000) return 20; // 1K+ pulls
|
||||
return 10; // Less popular models
|
||||
}
|
||||
|
||||
calculateEfficiencyScore(model, hardware) {
|
||||
const modelSize = this.extractModelSizeGB(model);
|
||||
const hardwareTier = this.calculateHardwareTier(hardware);
|
||||
|
||||
// Modelos más pequeños son más eficientes para hardware limitado
|
||||
if (hardwareTier === 'ultra_low' || hardwareTier === 'low') {
|
||||
if (modelSize <= 1) return 100;
|
||||
if (modelSize <= 3) return 80;
|
||||
if (modelSize <= 7) return 60;
|
||||
return 20;
|
||||
}
|
||||
|
||||
// Para hardware potente, balance entre tamaño y capacidad
|
||||
if (hardwareTier === 'high' || hardwareTier === 'ultra_high') {
|
||||
if (modelSize >= 13 && modelSize <= 30) return 100;
|
||||
if (modelSize >= 7 && modelSize <= 13) return 90;
|
||||
if (modelSize >= 3 && modelSize <= 7) return 80;
|
||||
return 60;
|
||||
}
|
||||
|
||||
// Hardware medio
|
||||
if (modelSize >= 3 && modelSize <= 13) return 100;
|
||||
if (modelSize <= 3) return 80;
|
||||
return 40;
|
||||
}
|
||||
|
||||
applyPenalties(score, model, hardware) {
|
||||
// Penalización por modelos muy grandes para hardware limitado
|
||||
const modelSize = this.extractModelSizeGB(model);
|
||||
const requiredRAM = this.estimateRAMRequirement(model);
|
||||
|
||||
if (requiredRAM > hardware.memory.total) {
|
||||
score -= 30; // Penalización severa
|
||||
}
|
||||
|
||||
if (modelSize > 30 && this.calculateHardwareTier(hardware) === 'low') {
|
||||
score -= 20; // Modelo demasiado grande
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
applyBonuses(score, model, category, hardware) {
|
||||
// Bonus por arquitectura Apple Silicon
|
||||
if (hardware.cpu.architecture === 'Apple Silicon') {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Bonus por modelos muy recientes (2024-2025)
|
||||
const currentYear = new Date().getFullYear();
|
||||
if (model.last_updated && model.last_updated.includes('day')) {
|
||||
score += 10; // Modelo muy reciente
|
||||
}
|
||||
|
||||
// Bonus por quantización disponible
|
||||
if (model.quantizations && model.quantizations.length > 0) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Bonus especial por categoría
|
||||
const categoryWeight = this.categories[category].weight;
|
||||
score *= categoryWeight;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
selectBestModels(scoredModels, hardwareTier, count = 3) {
|
||||
// Ordenar por score y seleccionar los mejores
|
||||
const sorted = scoredModels
|
||||
.sort((a, b) => b.categoryScore - a.categoryScore)
|
||||
.slice(0, count * 2); // Tomar más para diversidad
|
||||
|
||||
const selected = [];
|
||||
const seenBases = new Set();
|
||||
|
||||
// Seleccionar evitando duplicados de la misma base (ej: llama3.1:7b y llama3.1:8b)
|
||||
for (const model of sorted) {
|
||||
if (selected.length >= count) break;
|
||||
|
||||
const baseName = this.extractBaseName(model.model_identifier);
|
||||
if (!seenBases.has(baseName)) {
|
||||
selected.push(model);
|
||||
seenBases.add(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
// Si no tenemos suficientes, agregar más sin restricción
|
||||
if (selected.length < count) {
|
||||
const remaining = sorted.filter(m => !selected.includes(m));
|
||||
selected.push(...remaining.slice(0, count - selected.length));
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
extractBaseName(identifier) {
|
||||
// Extraer nombre base del modelo (ej: "llama3.1:7b" -> "llama3.1")
|
||||
return identifier.split(':')[0].split('-')[0];
|
||||
}
|
||||
|
||||
estimateRAMRequirement(model) {
|
||||
const size = this.extractModelSizeGB(model);
|
||||
// Estimación: modelo necesita ~1.2x su tamaño en RAM mínimo
|
||||
return Math.max(1, size * 1.2);
|
||||
}
|
||||
|
||||
estimateVRAMRequirement(model) {
|
||||
const size = this.extractModelSizeGB(model);
|
||||
// Para VRAM, modelos pequeños pueden correr solo en CPU
|
||||
if (size <= 3) return 0;
|
||||
return Math.max(2, size * 0.6);
|
||||
}
|
||||
|
||||
extractModelSizeGB(model) {
|
||||
// Intentar extraer tamaño de diferentes campos
|
||||
const sources = [
|
||||
model.model_identifier,
|
||||
model.model_name,
|
||||
...(model.model_sizes || []),
|
||||
...(model.tags || [])
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
if (!source) continue;
|
||||
const sizeMatch = source.match(/(\d+\.?\d*)\s*[bg]/i);
|
||||
if (sizeMatch) {
|
||||
const num = parseFloat(sizeMatch[1]);
|
||||
const unit = sizeMatch[0].slice(-1).toLowerCase();
|
||||
return unit === 'b' ? num : (unit === 'g' ? num : num);
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // Default size
|
||||
}
|
||||
|
||||
generateRecommendationSummary(recommendations, hardware) {
|
||||
const summary = {
|
||||
hardware_tier: this.calculateHardwareTier(hardware),
|
||||
total_categories: Object.keys(recommendations).length,
|
||||
best_overall: null,
|
||||
by_category: {},
|
||||
quick_commands: []
|
||||
};
|
||||
|
||||
let bestOverallScore = 0;
|
||||
let bestOverallModel = null;
|
||||
|
||||
Object.entries(recommendations).forEach(([category, data]) => {
|
||||
const bestModel = data.bestModels[0];
|
||||
if (bestModel) {
|
||||
summary.by_category[category] = {
|
||||
name: bestModel.model_name,
|
||||
identifier: bestModel.model_identifier,
|
||||
score: Math.round(bestModel.categoryScore),
|
||||
command: `ollama pull ${bestModel.model_identifier}`,
|
||||
size: this.extractModelSizeGB(bestModel) + 'B',
|
||||
pulls: bestModel.pulls
|
||||
};
|
||||
|
||||
summary.quick_commands.push(`ollama pull ${bestModel.model_identifier}`);
|
||||
|
||||
if (bestModel.categoryScore > bestOverallScore) {
|
||||
bestOverallScore = bestModel.categoryScore;
|
||||
bestOverallModel = bestModel;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (bestOverallModel) {
|
||||
summary.best_overall = {
|
||||
name: bestOverallModel.model_name,
|
||||
identifier: bestOverallModel.model_identifier,
|
||||
category: Object.keys(recommendations).find(cat =>
|
||||
recommendations[cat].bestModels.includes(bestOverallModel)
|
||||
),
|
||||
score: Math.round(bestOverallScore),
|
||||
command: `ollama pull ${bestOverallModel.model_identifier}`
|
||||
};
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IntelligentModelRecommender;
|
||||
|
|
@ -5,9 +5,11 @@ const path = require('path');
|
|||
class OllamaNativeScraper {
|
||||
constructor() {
|
||||
this.baseURL = 'https://ollama.com';
|
||||
this.registryAPI = 'https://registry.ollama.ai';
|
||||
this.cacheDir = path.join(__dirname, '.cache');
|
||||
this.cacheFile = path.join(this.cacheDir, 'ollama-models.json');
|
||||
this.cacheExpiry = 24 * 60 * 60 * 1000;
|
||||
this.detailedCacheFile = path.join(this.cacheDir, 'ollama-detailed-models.json');
|
||||
this.cacheExpiry = 6 * 60 * 60 * 1000; // 6 horas para actualizar más frecuentemente
|
||||
|
||||
if (!fs.existsSync(this.cacheDir)) {
|
||||
fs.mkdirSync(this.cacheDir, { recursive: true });
|
||||
|
|
@ -169,26 +171,405 @@ class OllamaNativeScraper {
|
|||
}
|
||||
}
|
||||
|
||||
async scrapeAllModels(forceRefresh = false) {
|
||||
isDetailedCacheValid() {
|
||||
if (!fs.existsSync(this.detailedCacheFile)) return false;
|
||||
const stats = fs.statSync(this.detailedCacheFile);
|
||||
const age = Date.now() - stats.mtime.getTime();
|
||||
return age < this.cacheExpiry;
|
||||
}
|
||||
|
||||
readDetailedCache() {
|
||||
try {
|
||||
if (!forceRefresh && this.isCacheValid()) {
|
||||
return this.readCache();
|
||||
}
|
||||
const data = fs.readFileSync(this.detailedCacheFile, 'utf8');
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await this.httpRequest(`${this.baseURL}/library`);
|
||||
if (response.statusCode !== 200) throw new Error(`Failed to fetch: ${response.statusCode}`);
|
||||
const models = this.parseModelFromHTML(response.data);
|
||||
if (models.length === 0) throw new Error('No models found');
|
||||
this.writeCache(models);
|
||||
|
||||
return {
|
||||
writeDetailedCache(models) {
|
||||
try {
|
||||
const data = {
|
||||
models,
|
||||
total_count: models.length,
|
||||
cached_at: new Date().toISOString(),
|
||||
expires_at: new Date(Date.now() + this.cacheExpiry).toISOString()
|
||||
};
|
||||
fs.writeFileSync(this.detailedCacheFile, JSON.stringify(data, null, 2));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getDetailedModelsInfo(basicModels) {
|
||||
const detailedModels = [];
|
||||
const batchSize = 5; // Procesar en lotes para no sobrecargar el servidor
|
||||
|
||||
for (let i = 0; i < basicModels.length; i += batchSize) {
|
||||
const batch = basicModels.slice(i, i + batchSize);
|
||||
console.log(`📦 Processing batch ${Math.floor(i/batchSize) + 1}/${Math.ceil(basicModels.length/batchSize)}`);
|
||||
|
||||
const batchPromises = batch.map(model => this.getModelDetailedInfo(model));
|
||||
const batchResults = await Promise.allSettled(batchPromises);
|
||||
|
||||
batchResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && result.value) {
|
||||
detailedModels.push(result.value);
|
||||
} else {
|
||||
// Si falla, al menos guardamos la información básica
|
||||
detailedModels.push(batch[index]);
|
||||
}
|
||||
});
|
||||
|
||||
// Pequeña pausa entre lotes
|
||||
if (i + batchSize < basicModels.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
|
||||
return detailedModels;
|
||||
}
|
||||
|
||||
async getModelDetailedInfo(basicModel) {
|
||||
try {
|
||||
const modelUrl = `${this.baseURL}/library/${basicModel.model_identifier}`;
|
||||
const response = await this.httpRequest(modelUrl);
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
return basicModel; // Fallback a información básica
|
||||
}
|
||||
|
||||
const detailedInfo = this.parseModelDetailPage(response.data, basicModel);
|
||||
|
||||
return {
|
||||
...basicModel,
|
||||
...detailedInfo,
|
||||
// Usar datos mejorados si están disponibles
|
||||
pulls: detailedInfo.actual_pulls || basicModel.pulls || 0,
|
||||
main_size: detailedInfo.main_size || 'Unknown',
|
||||
detailed_scraped_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
const cachedData = this.readCache();
|
||||
console.warn(`⚠️ Failed to get details for ${basicModel.model_identifier}: ${error.message}`);
|
||||
return basicModel; // Fallback a información básica
|
||||
}
|
||||
}
|
||||
|
||||
parseModelDetailPage(html, basicModel) {
|
||||
const details = {
|
||||
variants: [],
|
||||
tags: [],
|
||||
detailed_description: '',
|
||||
parameters: {},
|
||||
quantizations: [],
|
||||
model_sizes: [],
|
||||
category: 'general',
|
||||
use_cases: [],
|
||||
main_size: 'Unknown',
|
||||
actual_pulls: 0,
|
||||
context_length: 'Unknown',
|
||||
input_types: []
|
||||
};
|
||||
|
||||
try {
|
||||
// MEJORAR: Extraer TODOS los tags incluyendo quantizaciones específicas
|
||||
const allTagMatches = [];
|
||||
|
||||
// Buscar en bloques de código
|
||||
const codeBlocks = html.match(/<code[^>]*>([^<]+)<\/code>/g) || [];
|
||||
codeBlocks.forEach(match => {
|
||||
const content = match.replace(/<[^>]*>/g, '').trim();
|
||||
const modelMatch = content.match(/ollama (?:run|pull) ([^\s]+)/);
|
||||
if (modelMatch) {
|
||||
allTagMatches.push(modelMatch[1]);
|
||||
}
|
||||
});
|
||||
|
||||
// Buscar en texto plano (para tags que no están en código)
|
||||
const plainTextTags = html.match(new RegExp(`${basicModel.model_identifier}:[\\w\\d\\.-]+`, 'g')) || [];
|
||||
allTagMatches.push(...plainTextTags);
|
||||
|
||||
// Buscar patrones específicos de quantización
|
||||
const quantPatterns = [
|
||||
new RegExp(`${basicModel.model_identifier}:[\\w\\d\\.-]*q\\d+[_km\\d]*`, 'gi'),
|
||||
new RegExp(`${basicModel.model_identifier}:[\\w\\d\\.-]*fp\\d+`, 'gi'),
|
||||
new RegExp(`${basicModel.model_identifier}:[\\w\\d\\.-]*int\\d+`, 'gi')
|
||||
];
|
||||
|
||||
quantPatterns.forEach(pattern => {
|
||||
const matches = html.match(pattern) || [];
|
||||
allTagMatches.push(...matches);
|
||||
});
|
||||
|
||||
// Limpiar y deduplicar tags
|
||||
details.tags = [...new Set(allTagMatches)]
|
||||
.filter(tag => tag && tag.includes(':'))
|
||||
.slice(0, 50); // Aumentar límite para capturar más variantes
|
||||
|
||||
// NUEVO: Extraer información de contexto
|
||||
const contextMatches = html.match(/context\s*:?\s*(\d+[kmb]?)/gi) ||
|
||||
html.match(/(\d+[kmb]?)\s*context/gi) ||
|
||||
html.match(/context\s+length\s*:?\s*(\d+[kmb]?)/gi);
|
||||
|
||||
if (contextMatches && contextMatches.length > 0) {
|
||||
// Extraer el número más grande encontrado
|
||||
const contextNumbers = contextMatches.map(match => {
|
||||
const num = match.match(/(\d+[kmb]?)/i);
|
||||
if (num) {
|
||||
const value = num[1].toLowerCase();
|
||||
if (value.includes('k')) return parseInt(value) * 1000;
|
||||
if (value.includes('m')) return parseInt(value) * 1000000;
|
||||
if (value.includes('b')) return parseInt(value) * 1000000000;
|
||||
return parseInt(value);
|
||||
}
|
||||
return 0;
|
||||
}).filter(n => n > 0);
|
||||
|
||||
if (contextNumbers.length > 0) {
|
||||
const maxContext = Math.max(...contextNumbers);
|
||||
details.context_length = maxContext > 1000000 ?
|
||||
`${(maxContext/1000000).toFixed(1)}M` :
|
||||
maxContext > 1000 ? `${(maxContext/1000).toFixed(0)}K` :
|
||||
maxContext.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// NUEVO: Detectar tipos de input soportados
|
||||
const inputTypes = [];
|
||||
if (html.toLowerCase().includes('text') || html.toLowerCase().includes('chat')) {
|
||||
inputTypes.push('text');
|
||||
}
|
||||
if (html.toLowerCase().includes('image') || html.toLowerCase().includes('vision') ||
|
||||
html.toLowerCase().includes('visual')) {
|
||||
inputTypes.push('image');
|
||||
}
|
||||
if (html.toLowerCase().includes('code') || html.toLowerCase().includes('programming')) {
|
||||
inputTypes.push('code');
|
||||
}
|
||||
if (html.toLowerCase().includes('audio') || html.toLowerCase().includes('speech')) {
|
||||
inputTypes.push('audio');
|
||||
}
|
||||
|
||||
details.input_types = inputTypes.length > 0 ? inputTypes : ['text'];
|
||||
|
||||
// Mejor extracción de tamaños con regex más específico
|
||||
const sizeMatches = html.match(/\b(\d+(?:\.\d+)?)\s*[BG]B?\b/gi);
|
||||
if (sizeMatches) {
|
||||
details.model_sizes = [...new Set(sizeMatches.map(size => size.toLowerCase()))];
|
||||
// Determinar el tamaño principal (más común)
|
||||
if (details.model_sizes.length > 0) {
|
||||
details.main_size = details.model_sizes[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Extraer pulls reales del HTML
|
||||
const pullsMatch = html.match(/(\d+(?:\.\d+)?[KMB]?)\s*pulls?/i);
|
||||
if (pullsMatch) {
|
||||
details.actual_pulls = this.parsePulls(pullsMatch[1]);
|
||||
}
|
||||
|
||||
// Mejorar detección de quantizaciones
|
||||
const quantMatches = html.match(/\b(Q\d+_[KM](?:_[MS])?|Q\d+|FP16|FP32|INT8|INT4)\b/gi);
|
||||
if (quantMatches) {
|
||||
details.quantizations = [...new Set(quantMatches.map(q => q.toUpperCase()))];
|
||||
}
|
||||
|
||||
// Mejor categorización basada en múltiples indicadores
|
||||
const htmlLower = html.toLowerCase();
|
||||
const title = html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.toLowerCase() || '';
|
||||
const description = html.match(/<meta[^>]*name="description"[^>]*content="([^"]+)"/i)?.[1]?.toLowerCase() || '';
|
||||
const fullText = `${htmlLower} ${title} ${description}`;
|
||||
|
||||
// Resetear categoría
|
||||
details.category = 'general';
|
||||
details.use_cases = [];
|
||||
|
||||
// Categorizar basado en el nombre del modelo de forma más robusta
|
||||
const modelName = basicModel.model_identifier.toLowerCase();
|
||||
const modelDisplayName = basicModel.model_name.toLowerCase();
|
||||
const fullModelText = `${modelName} ${modelDisplayName}`;
|
||||
|
||||
// Resetear categoría y casos de uso
|
||||
details.category = 'general';
|
||||
details.use_cases = [];
|
||||
|
||||
// Sistema de categorización por prioridad (específico a general)
|
||||
|
||||
// 1. CODING - Detectar modelos de programación
|
||||
if (fullModelText.includes('coder') ||
|
||||
fullModelText.includes('codellama') ||
|
||||
fullModelText.includes('starcoder') ||
|
||||
fullModelText.includes('codestral') ||
|
||||
fullModelText.includes('code-') ||
|
||||
modelName.startsWith('codellama') ||
|
||||
modelName.startsWith('starcoder') ||
|
||||
modelName.includes('deepseek-coder') ||
|
||||
modelName.includes('qwen2.5-coder')) {
|
||||
details.category = 'coding';
|
||||
details.use_cases.push('coding', 'programming', 'development');
|
||||
}
|
||||
|
||||
// 2. EMBEDDINGS - Modelos de vectores/embeddings
|
||||
else if (fullModelText.includes('embed') ||
|
||||
fullModelText.includes('nomic') ||
|
||||
fullModelText.includes('bge') ||
|
||||
fullModelText.includes('e5') ||
|
||||
modelName.includes('all-minilm') ||
|
||||
modelName.startsWith('nomic-embed')) {
|
||||
details.category = 'embeddings';
|
||||
details.use_cases.push('embeddings', 'search', 'similarity');
|
||||
}
|
||||
|
||||
// 3. MULTIMODAL - Modelos de visión/imagen
|
||||
else if (fullModelText.includes('llava') ||
|
||||
fullModelText.includes('pixtral') ||
|
||||
fullModelText.includes('vision') ||
|
||||
fullModelText.includes('moondream') ||
|
||||
modelName.includes('qwen-vl') ||
|
||||
modelName.includes('qwen2.5vl') ||
|
||||
modelName.startsWith('llava')) {
|
||||
details.category = 'multimodal';
|
||||
details.use_cases.push('vision', 'multimodal', 'image');
|
||||
}
|
||||
|
||||
// 4. REASONING - Modelos especializados en razonamiento
|
||||
else if (fullModelText.includes('deepseek-r1') ||
|
||||
fullModelText.includes('reasoning') ||
|
||||
fullModelText.includes('math') ||
|
||||
modelName.includes('deepseek-r1') ||
|
||||
modelName.includes('o1-')) {
|
||||
details.category = 'reasoning';
|
||||
details.use_cases.push('reasoning', 'mathematics', 'logic');
|
||||
}
|
||||
|
||||
// 5. TALKING - Modelos conversacionales/chat (mayoría de modelos)
|
||||
else if (fullModelText.includes('llama') ||
|
||||
fullModelText.includes('mistral') ||
|
||||
fullModelText.includes('phi') ||
|
||||
fullModelText.includes('gemma') ||
|
||||
fullModelText.includes('qwen') ||
|
||||
fullModelText.includes('chat') ||
|
||||
fullModelText.includes('instruct') ||
|
||||
modelName.startsWith('llama') ||
|
||||
modelName.startsWith('mistral') ||
|
||||
modelName.startsWith('phi') ||
|
||||
modelName.startsWith('gemma') ||
|
||||
modelName.startsWith('qwen') && !modelName.includes('coder') && !modelName.includes('vl')) {
|
||||
details.category = 'talking';
|
||||
details.use_cases.push('chat', 'conversation', 'assistant');
|
||||
}
|
||||
|
||||
// 6. READING - Modelos para análisis de texto
|
||||
else if (fullModelText.includes('solar') ||
|
||||
fullModelText.includes('openchat') ||
|
||||
fullModelText.includes('neural-chat') ||
|
||||
fullModelText.includes('vicuna')) {
|
||||
details.category = 'reading';
|
||||
details.use_cases.push('reading', 'analysis', 'comprehension');
|
||||
}
|
||||
|
||||
// 7. CREATIVE - Modelos creativos
|
||||
else if (fullModelText.includes('dolphin') ||
|
||||
fullModelText.includes('wizard') ||
|
||||
fullModelText.includes('uncensored') ||
|
||||
fullModelText.includes('airoboros')) {
|
||||
details.category = 'creative';
|
||||
details.use_cases.push('creative', 'writing', 'storytelling');
|
||||
}
|
||||
|
||||
// 8. Por defecto: GENERAL
|
||||
else {
|
||||
details.category = 'general';
|
||||
details.use_cases.push('general', 'assistant');
|
||||
}
|
||||
|
||||
// Extraer descripción mejorada
|
||||
const descPatterns = [
|
||||
/<p[^>]*class="[^"]*description[^"]*"[^>]*>([^<]+)<\/p>/i,
|
||||
/<meta[^>]*name="description"[^>]*content="([^"]+)"/i,
|
||||
/<div[^>]*class="[^"]*desc[^"]*"[^>]*>([^<]+)<\/div>/i
|
||||
];
|
||||
|
||||
for (const pattern of descPatterns) {
|
||||
const match = html.match(pattern);
|
||||
if (match) {
|
||||
details.detailed_description = this.cleanText(match[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Crear variantes mejoradas
|
||||
details.variants = details.tags.map(tag => {
|
||||
const size = this.extractSizeFromTag(tag);
|
||||
const quantization = this.extractQuantizationFromTag(tag);
|
||||
return {
|
||||
tag: tag,
|
||||
size: size,
|
||||
quantization: quantization,
|
||||
command: `ollama pull ${tag}`,
|
||||
estimated_size_gb: this.estimateModelSizeGB(tag)
|
||||
};
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Error parsing detailed page: ${error.message}`);
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
extractSizeFromTag(tag) {
|
||||
const sizeMatch = tag.match(/(\d+\.?\d*)[bg]/i);
|
||||
return sizeMatch ? sizeMatch[0].toLowerCase() : 'unknown';
|
||||
}
|
||||
|
||||
extractQuantizationFromTag(tag) {
|
||||
const quantMatch = tag.match(/\b(q\d+_[km]?_?[ms]?|fp16|fp32|int8|int4)\b/i);
|
||||
return quantMatch ? quantMatch[0].toUpperCase() : 'Q4_0'; // Default assumption
|
||||
}
|
||||
|
||||
estimateModelSizeGB(tag) {
|
||||
const sizeMatch = tag.match(/(\d+\.?\d*)[bg]/i);
|
||||
if (!sizeMatch) return 1;
|
||||
|
||||
const num = parseFloat(sizeMatch[1]);
|
||||
const unit = sizeMatch[0].slice(-1).toLowerCase();
|
||||
|
||||
if (unit === 'b') return num;
|
||||
if (unit === 'g') return num;
|
||||
return num; // Default to GB
|
||||
}
|
||||
|
||||
async scrapeAllModels(forceRefresh = false) {
|
||||
try {
|
||||
if (!forceRefresh && this.isDetailedCacheValid()) {
|
||||
return this.readDetailedCache();
|
||||
}
|
||||
|
||||
console.log('🔍 Scraping ALL Ollama models with detailed information...');
|
||||
|
||||
// Primero obtenemos la lista básica de modelos
|
||||
const response = await this.httpRequest(`${this.baseURL}/library`);
|
||||
if (response.statusCode !== 200) throw new Error(`Failed to fetch: ${response.statusCode}`);
|
||||
const basicModels = this.parseModelFromHTML(response.data);
|
||||
|
||||
console.log(`📋 Found ${basicModels.length} models. Getting detailed information...`);
|
||||
|
||||
// Ahora obtenemos información detallada de cada modelo
|
||||
const detailedModels = await this.getDetailedModelsInfo(basicModels);
|
||||
|
||||
this.writeDetailedCache(detailedModels);
|
||||
|
||||
return {
|
||||
models: detailedModels,
|
||||
total_count: detailedModels.length,
|
||||
cached_at: new Date().toISOString(),
|
||||
expires_at: new Date(Date.now() + this.cacheExpiry).toISOString()
|
||||
};
|
||||
} catch (error) {
|
||||
const cachedData = this.readDetailedCache();
|
||||
if (cachedData) return cachedData;
|
||||
throw error;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue