-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.js
More file actions
312 lines (283 loc) · 14 KB
/
Copy pathsetup.js
File metadata and controls
312 lines (283 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#!/usr/bin/env node
/**
* AEO & LLM Citation Graph Simulator — Setup Wizard
*
* Interactively configures the tool for REAL, verified production use:
* 1. Collects live API keys (OpenAI, Anthropic, Google, Perplexity, DeepSeek)
* and validates each with a real, minimal API call before writing .env.
* 2. Collects your brand + competitor ground truth and generates
* config/entity_maps.json.
* 3. Writes .env from .env.example defaults.
*
* Nothing is fabricated: if a key fails live validation you are told
* immediately and the key is not written to disk.
*/
import fs from 'node:fs';
import path from 'node:path';
import readline from 'node:readline/promises';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
function cyan(s) { return `\x1b[36m${s}\x1b[0m`; }
function green(s) { return `\x1b[32m${s}\x1b[0m`; }
function yellow(s) { return `\x1b[33m${s}\x1b[0m`; }
function red(s) { return `\x1b[31m${s}\x1b[0m`; }
function bold(s) { return `\x1b[1m${s}\x1b[0m`; }
async function ask(question, defaultValue = '') {
const suffix = defaultValue ? ` [${defaultValue}]` : '';
const answer = await rl.question(`${cyan('?')} ${question}${suffix}: `);
return answer.trim() || defaultValue;
}
async function askYesNo(question, defaultValue = true) {
const suffix = defaultValue ? ' [Y/n]' : ' [y/N]';
const answer = (await rl.question(`${cyan('?')} ${question}${suffix}: `)).trim().toLowerCase();
if (answer === 'y' || answer === 'yes') return true;
if (answer === 'n' || answer === 'no') return false;
return defaultValue;
}
// ── Python version + Lite/Full guard (enterprise reproducibility) ──────────
function checkPython() {
const cands = process.env.AEO_PYTHON ? [process.env.AEO_PYTHON] : ['python', 'python3', 'py'];
for (const cmd of cands) {
try {
const out = execFileSync(cmd, ['--version'], { encoding: 'utf8', timeout: 8000 });
const m = out.match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
if (!m) continue;
const [major, minor] = [Number(m[1]), Number(m[2])];
const ok = major > 3 || (major === 3 && minor >= 11);
console.log(`${cyan('i')} Python: ${out.trim()} (${cmd}) ${ok ? green('OK (>=3.11)') : red('TOO OLD — requires >=3.11, see .python-version')}`);
if (!ok) process.exitCode = 1;
try {
execFileSync(cmd, ['-c', 'import torch, transformers'], { stdio: 'ignore', timeout: 8000 });
console.log(`${cyan('i')} ML profile: ${green('FULL')} (torch/transformers present — RoBERTa/MiniLM-trf enabled)`);
} catch {
console.log(`${cyan('i')} ML profile: ${yellow('LITE')} (requirements-lite.txt — MiniLM/keyword + VADER fallbacks; dashboard badges "Lite mode: transformer sentiment OFF")`);
}
return cmd;
} catch {}
}
console.log(yellow(' Warning: no python found on PATH — install Python >=3.11 before running the pipeline.'));
return null;
}
// ── Live key validation ───────────────────────────────────────────────────
async function validateOpenAI(key) {
const { default: OpenAI } = await import('openai');
const client = new OpenAI({ apiKey: key });
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Reply with exactly: ok' }],
max_tokens: 5
});
return (res.choices?.[0]?.message?.content || '').toLowerCase().includes('ok');
}
async function validateAnthropic(key) {
const { Anthropic } = await import('@anthropic-ai/sdk');
const client = new Anthropic({ apiKey: key });
const res = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 8,
messages: [{ role: 'user', content: 'Reply with exactly: ok' }]
});
const text = res.content?.[0]?.text || '';
return text.toLowerCase().includes('ok');
}
async function validateGoogle(key) {
const { GoogleGenerativeAI } = await import('@google/generative-ai');
const genAI = new GoogleGenerativeAI(key);
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
const res = await model.generateContent('Reply with exactly: ok');
const text = res.response?.text?.() || '';
return text.toLowerCase().includes('ok');
}
async function validateOpenAICompat(key, baseURL, label) {
const { default: OpenAI } = await import('openai');
const client = new OpenAI({ apiKey: key, baseURL });
const res = await client.chat.completions.create({
model: 'sonar',
messages: [{ role: 'user', content: 'Reply with exactly: ok' }],
max_tokens: 5
});
return (res.choices?.[0]?.message?.content || '').toLowerCase().includes('ok');
}
async function validateKey(provider, key) {
process.stdout.write(` Validating ${provider} key... `);
try {
let ok = false;
if (provider === 'OPENAI') ok = await validateOpenAI(key);
else if (provider === 'ANTHROPIC') ok = await validateAnthropic(key);
else if (provider === 'GOOGLE_AI') ok = await validateGoogle(key);
else if (provider === 'PERPLEXITY') ok = await validateOpenAICompat(key, 'https://api.perplexity.ai', 'Perplexity');
else if (provider === 'DEEPSEEK') ok = await validateOpenAICompat(key, 'https://api.deepseek.com', 'DeepSeek');
if (ok) { process.stdout.write(green('OK\n')); return true; }
process.stdout.write(red('FAILED (no valid response)\n'));
return false;
} catch (err) {
process.stdout.write(red(`FAILED (${err?.status || err?.statusCode || err?.message || err})\n`));
return false;
}
}
// ── entity_maps.json generation ───────────────────────────────────────────
function buildEntityMaps(answers) {
const attributes = {};
if (answers.brand.category) attributes.category = answers.brand.category;
if (answers.brand.features) attributes.features = answers.brand.features.split(';').map(s => s.trim()).filter(Boolean);
if (answers.brand.pricing) attributes.pricing_model = answers.brand.pricing.trim();
if (answers.brand.certifications) attributes.certifications = answers.brand.certifications.split(';').map(s => s.trim()).filter(Boolean);
if (answers.brand.usps) attributes.unique_selling_points = answers.brand.usps.split(';').map(s => s.trim()).filter(Boolean);
if (answers.brand.target_segment) attributes.target_segment = answers.brand.target_segment.split(';').map(s => s.trim()).filter(Boolean);
const competitors = [];
for (const comp of answers.competitors) {
const c = { primary_name: comp.name };
if (comp.aliases) c.aliases = comp.aliases.split(';').map(s => s.trim()).filter(Boolean);
const compAttrs = {};
if (comp.features) compAttrs.features = comp.features.split(';').map(s => s.trim()).filter(Boolean);
if (comp.pricing) compAttrs.pricing_model = comp.pricing.trim();
if (compAttrs.features || compAttrs.pricing_model) c.attributes = compAttrs;
competitors.push(c);
}
return {
entity_maps: {
your_brand: {
primary_name: answers.brand.name,
aliases: (answers.brand.aliases || '').split(';').map(s => s.trim()).filter(Boolean),
attributes
},
competitors,
target_queries: [
`who is the best ${answers.brand.category || 'alternative'}`,
`alternatives to ${answers.brand.name}`,
`${answers.brand.category || 'software'} comparison`,
`reviews of ${answers.brand.name}`
]
}
};
}
// ── main ───────────────────────────────────────────────────────────────────
async function main() {
console.log('\n' + bold('=============================================='));
console.log(bold('AEO & LLM Citation Graph Simulator — Setup Wizard'));
console.log(bold('=============================================='));
console.log(yellow('Configures the tool for real, verified, production-grade LLM data.'));
console.log(yellow('Every API key is validated live before it is saved.\n'));
checkPython();
if (fs.existsSync(path.join(ROOT, '.env'))) {
console.log(yellow('An .env file already exists.'));
if (!(await askYesNo('Overwrite it?', false))) {
console.log(red('Aborting. Your existing .env was left untouched.'));
rl.close();
return;
}
}
// 1. API keys
const keys = {};
const providers = [
{ env: 'OPENAI_API_KEY', label: 'OpenAI (gpt-4o / gpt-4o-mini)', validate: 'OPENAI', optional: false },
{ env: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude 3.5/3.7 Sonnet)', validate: 'ANTHROPIC', optional: true },
{ env: 'GOOGLE_AI_API_KEY', label: 'Google AI Studio (Gemini)', validate: 'GOOGLE_AI', optional: true },
{ env: 'PERPLEXITY_API_KEY', label: 'Perplexity (Sonar Pro)', validate: 'PERPLEXITY', optional: true },
{ env: 'DEEPSEEK_API_KEY', label: 'DeepSeek', validate: 'DEEPSEEK', optional: true }
];
console.log(bold('\n[1/3] API Keys (validated live)\n'));
for (const p of providers) {
const hint = p.optional ? ' (optional — skip to disable this provider)' : '';
let key = '';
while (!key) {
const answer = await ask(`${p.label}${hint}`);
if (!answer && p.optional) { key = 'SKIPPED'; break; }
if (!answer) { console.log(red(' OpenAI is required. Enter a valid key or press Ctrl+C to quit.')); continue; }
const ok = await validateKey(p.validate, answer);
if (ok) { key = answer; }
else if (p.optional) {
if (!(await askYesNo('Validation failed. Add it anyway (unvalidated)?', false))) {
console.log(yellow(' Skipping this provider.'));
key = 'SKIPPED';
} else {
key = answer;
}
}
}
keys[p.env] = key;
}
// 2. Brand ground truth
console.log(bold('\n[2/3] Your Brand Ground Truth (used for claim verification)\n'));
const brand = {
name: await ask('Your brand primary name'),
aliases: await ask('Aliases / alternate spellings (semicolon-separated, optional)'),
category: await ask('Product category (e.g. "project management software")'),
features: await ask('Key features (semicolon-separated)'),
pricing: await ask('Pricing model (e.g. "free tier with paid plans from $10/month")'),
certifications: await ask('Certifications/compliance (semicolon-separated, optional)'),
usps: await ask('Unique selling points (semicolon-separated)'),
target_segment: await ask('Target segments (semicolon-separated, optional)')
};
const competitors = [];
console.log(bold('\nCompetitors (press Enter on name to finish)\n'));
while (true) {
const name = await ask(`Competitor ${competitors.length + 1} name (Enter to finish)`);
if (!name) break;
const comp = { name };
comp.aliases = await ask(' Aliases (optional)');
comp.features = await ask(' Key features (optional)');
comp.pricing = await ask(' Pricing model (optional)');
competitors.push(comp);
}
// 3. Execution settings
console.log(bold('\n[3/3] Execution Settings\n'));
const settings = {
maxConcurrent: await ask('Max concurrent API requests', '10'),
maxTokens: await ask('Max tokens per response', '4096'),
budgetUsd: await ask('Daily API budget (USD)', '20.00'),
promptCount: await ask('Prompts per run (recommended 50-150)', '50')
};
// Write .env
const envPath = path.join(ROOT, '.env');
const example = fs.readFileSync(path.join(ROOT, '.env.example'), 'utf8');
const lines = example.split('\n').map((line, i, arr) => {
const m = line.match(/^([A-Z_0-9]+)=(.*)$/);
if (!m) return line;
const envName = m[1];
if (keys[envName] && keys[envName] !== 'SKIPPED') return `${envName}=${keys[envName]}`;
if (envName === 'MAX_CONCURRENT_REQUESTS') return `${envName}=${settings.maxConcurrent}`;
if (envName === 'MAX_TOKENS') return `${envName}=${settings.maxTokens}`;
if (envName === 'DAILY_BUDGET_USD') return `${envName}=${settings.budgetUsd}`;
return line;
});
fs.writeFileSync(envPath, lines.join('\n'), 'utf8');
console.log(green(`\n✓ .env written to ${envPath}`));
// Write entity_maps.json
const entityMapsPath = path.join(ROOT, 'config', 'entity_maps.json');
fs.writeFileSync(entityMapsPath, JSON.stringify(buildEntityMaps({ brand, competitors }), null, 2), 'utf8');
console.log(green(`✓ entity_maps.json written to ${entityMapsPath}`));
// Update execution.json default prompt count if requested
const execPath = path.join(ROOT, 'config', 'execution.json');
if (fs.existsSync(execPath)) {
try {
const exec = JSON.parse(fs.readFileSync(execPath, 'utf8'));
if (exec.execution && typeof exec.execution.prompt_count === 'number') {
exec.execution.prompt_count = parseInt(settings.promptCount, 10) || 50;
fs.writeFileSync(execPath, JSON.stringify(exec, null, 2), 'utf8');
console.log(green(`✓ prompt_count updated in config/execution.json (${settings.promptCount})`));
}
} catch (e) {
console.log(yellow(` Warning: could not update execution.json (${e.message})`));
}
}
const enabled = Object.entries(keys).filter(([, v]) => v && v !== 'SKIPPED').map(([k]) => k.replace('_API_KEY', ''));
console.log(bold('\n=============================================='));
console.log(bold('Setup complete'));
console.log(bold('=============================================='));
console.log(` Enabled providers: ${enabled.length ? enabled.join(', ') : 'NONE'}`);
console.log(` Brand: ${brand.name}${competitors.length ? ` (+ ${competitors.length} competitors)` : ''}`);
console.log('\n Next steps:');
console.log(' 1. Validate configuration: npm run validate');
console.log(' 2. Start a real run: npm run full-run');
console.log(' 3. For RAG split testing: set RAG_ENABLED=true in .env\n');
rl.close();
}
main().catch((err) => {
console.error(red(`\nSetup failed: ${err.message}`));
process.exitCode = 1;
rl.close();
});