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:
Pavelevich 2025-08-05 10:26:53 +02:00
parent 3aa6ffb183
commit 84381e4ac7
6 changed files with 1271 additions and 16 deletions

1
.npmrc
View file

@ -1 +0,0 @@
prefix=/home/pavel/.npm-global

View file

@ -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();

View file

@ -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",

View file

@ -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;

View 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;

View file

@ -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;
}