Files
mek-tech-consulting/lib/ai.js
T

266 lines
8.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { getDb } = require('../db');
const AI_CONFIG_PATH = path.join(__dirname, '..', 'data', 'ai_config.json');
const PROVIDER_PRESETS = {
openrouter: {
label: 'OpenRouter',
base_url: 'https://openrouter.ai/api/v1',
models: ['qwen/qwen3-coder:free', 'meta-llama/llama-3.3-70b-instruct:free', 'google/gemma-3-27b-it:free', 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o-mini']
},
openai: {
label: 'OpenAI',
base_url: 'https://api.openai.com/v1',
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-mini']
},
anthropic: {
label: 'Anthropic',
base_url: 'https://api.anthropic.com/v1',
models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229']
},
deepseek: {
label: 'DeepSeek',
base_url: 'https://api.deepseek.com/v1',
models: ['deepseek-chat', 'deepseek-reasoner']
},
groq: {
label: 'Groq',
base_url: 'https://api.groq.com/openai/v1',
models: ['llama-3.3-70b-versatile', 'mixtral-8x7b-32768']
},
ollama: {
label: 'Ollama (local)',
base_url: 'http://localhost:11434/v1',
models: ['llama3.2', 'mistral', 'codellama', 'qwen2.5']
},
azure: {
label: 'Azure OpenAI',
base_url: '',
models: ['gpt-4o', 'gpt-4o-mini']
},
custom: {
label: 'Custom (OpenAI-compatible)',
base_url: '',
models: []
}
};
function initAiSchema() {
const db = getDb();
db.exec(`
CREATE TABLE IF NOT EXISTS ai_providers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider_type TEXT NOT NULL DEFAULT 'openrouter',
api_key TEXT DEFAULT '',
base_url TEXT DEFAULT '',
default_model TEXT DEFAULT '',
is_active INTEGER DEFAULT 1,
is_default INTEGER DEFAULT 0,
extra_headers TEXT DEFAULT '{}',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS integrations (
id TEXT PRIMARY KEY,
name TEXT DEFAULT '',
api_key TEXT DEFAULT '',
webhook_url TEXT DEFAULT '',
config TEXT DEFAULT '{}',
status TEXT DEFAULT 'inactive',
created_at TEXT DEFAULT (datetime('now'))
);
`);
const count = db.prepare('SELECT COUNT(*) as c FROM ai_providers').get().c;
if (count === 0 && process.env.DEEPSEEK_API_KEY) {
db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_default)
VALUES (?, ?, ?, ?, ?, 1)`).run(
'OpenRouter Default', 'openrouter', process.env.DEEPSEEK_API_KEY,
'https://openrouter.ai/api/v1', 'qwen/qwen3-coder:free'
);
}
}
let _aiSchemaReady = false;
function ensureAiSchema() {
if (_aiSchemaReady) return;
initAiSchema();
_aiSchemaReady = true;
}
function getProviders() {
ensureAiSchema();
return getDb().prepare('SELECT id, name, provider_type, base_url, default_model, is_active, is_default, created_at, updated_at FROM ai_providers ORDER BY is_default DESC, name').all();
}
function getProvider(id) {
ensureAiSchema();
return getDb().prepare('SELECT * FROM ai_providers WHERE id = ?').get(id);
}
function getDefaultProvider() {
ensureAiSchema();
let p = getDb().prepare('SELECT * FROM ai_providers WHERE is_default = 1 AND is_active = 1 LIMIT 1').get();
if (!p) p = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1 ORDER BY id LIMIT 1').get();
return p;
}
function saveProvider(data) {
ensureAiSchema();
const db = getDb();
if (data.is_default) {
db.prepare('UPDATE ai_providers SET is_default = 0').run();
}
if (data.id) {
db.prepare(`UPDATE ai_providers SET name=?, provider_type=?, api_key=?, base_url=?, default_model=?,
is_active=?, is_default=?, updated_at=datetime('now') WHERE id=?`).run(
data.name, data.provider_type, data.api_key || '', data.base_url || '',
data.default_model || '', data.is_active ? 1 : 0, data.is_default ? 1 : 0, data.id
);
syncAiConfigFile();
return data.id;
}
const r = db.prepare(`INSERT INTO ai_providers (name, provider_type, api_key, base_url, default_model, is_active, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
data.name, data.provider_type, data.api_key || '', data.base_url || '',
data.default_model || '', data.is_active !== false ? 1 : 0, data.is_default ? 1 : 0
);
syncAiConfigFile();
return r.lastInsertRowid;
}
function deleteProvider(id) {
ensureAiSchema();
getDb().prepare('DELETE FROM ai_providers WHERE id = ?').run(id);
syncAiConfigFile();
}
function syncAiConfigFile() {
ensureAiSchema();
const providers = getDb().prepare('SELECT * FROM ai_providers WHERE is_active = 1').all();
const defaultProvider = providers.find(p => p.is_default) || providers[0] || null;
const config = {
default_provider_id: defaultProvider?.id || null,
providers: providers.map(p => ({
id: p.id,
name: p.name,
provider_type: p.provider_type,
api_key: p.api_key,
base_url: p.base_url || PROVIDER_PRESETS[p.provider_type]?.base_url || '',
default_model: p.default_model,
is_default: !!p.is_default
}))
};
const dir = path.dirname(AI_CONFIG_PATH);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2));
const leadgenConfig = {
api_key: defaultProvider?.api_key || process.env.DEEPSEEK_API_KEY || '',
base_url: defaultProvider?.base_url || 'https://openrouter.ai/api/v1',
model: defaultProvider?.default_model || 'qwen/qwen3-coder:free',
provider_type: defaultProvider?.provider_type || 'openrouter'
};
fs.writeFileSync(path.join(dir, 'leadgen_config.json'), JSON.stringify(leadgenConfig, null, 2));
}
function maskKey(key) {
if (!key || key.length < 8) return key ? '••••••••' : '';
return key.slice(0, 6) + '••••' + key.slice(-4);
}
function chatCompletion(messages, options = {}) {
return new Promise((resolve, reject) => {
const provider = options.providerId
? getProvider(options.providerId)
: getDefaultProvider();
if (!provider || !provider.api_key) {
return reject(new Error('Geen actieve AI provider geconfigureerd. Ga naar /ai om een provider toe te voegen.'));
}
const preset = PROVIDER_PRESETS[provider.provider_type] || PROVIDER_PRESETS.custom;
const baseUrl = provider.base_url || preset.base_url;
const model = options.model || provider.default_model || preset.models[0];
let url;
try {
url = new URL('/chat/completions', baseUrl.endsWith('/') ? baseUrl : baseUrl + '/');
} catch (e) {
return reject(new Error('Ongeldige base URL: ' + baseUrl));
}
const body = JSON.stringify({
model,
messages,
temperature: options.temperature ?? 0.7,
max_tokens: options.max_tokens ?? 4096
});
const isHttps = url.protocol === 'https:';
const lib = isHttps ? https : http;
const headers = {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body)
};
if (provider.provider_type === 'anthropic') {
headers['x-api-key'] = provider.api_key;
headers['anthropic-version'] = '2023-06-01';
} else {
headers['Authorization'] = `Bearer ${provider.api_key}`;
}
if (provider.provider_type === 'openrouter') {
headers['HTTP-Referer'] = process.env.BASE_URL || 'http://localhost:3000';
headers['X-Title'] = 'Mek-Tech Platform';
}
const reqOpts = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: 'POST',
headers,
timeout: 120000
};
const req = lib.request(reqOpts, (res) => {
let data = '';
res.on('data', c => { data += c; });
res.on('end', () => {
try {
const parsed = JSON.parse(data);
if (res.statusCode >= 400) {
reject(new Error(parsed.error?.message || parsed.message || data.slice(0, 200)));
} else {
resolve(parsed);
}
} catch (e) {
reject(new Error('AI response parse error: ' + data.slice(0, 200)));
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('AI request timeout')); });
req.write(body);
req.end();
});
}
module.exports = {
PROVIDER_PRESETS,
initAiSchema: ensureAiSchema,
getProviders,
getProvider,
getDefaultProvider,
saveProvider,
deleteProvider,
syncAiConfigFile,
maskKey,
chatCompletion
};