Initial commit — Mek-Tech AI Consultancy Framework v2.0
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user