wayfind 0.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BOOTSTRAP_PROMPT.md +120 -0
- package/bin/connectors/github.js +617 -0
- package/bin/connectors/index.js +13 -0
- package/bin/connectors/intercom.js +595 -0
- package/bin/connectors/llm.js +469 -0
- package/bin/connectors/notion.js +747 -0
- package/bin/connectors/transport.js +325 -0
- package/bin/content-store.js +2006 -0
- package/bin/digest.js +813 -0
- package/bin/rebuild-status.js +297 -0
- package/bin/slack-bot.js +1535 -0
- package/bin/slack.js +342 -0
- package/bin/storage/index.js +171 -0
- package/bin/storage/json-backend.js +348 -0
- package/bin/storage/sqlite-backend.js +415 -0
- package/bin/team-context.js +4209 -0
- package/bin/telemetry.js +159 -0
- package/doctor.sh +291 -0
- package/install.sh +144 -0
- package/journal-summary.sh +577 -0
- package/package.json +48 -6
- package/setup.sh +641 -0
- package/specializations/claude-code/CLAUDE.md-global-fragment.md +53 -0
- package/specializations/claude-code/CLAUDE.md-repo-fragment.md +16 -0
- package/specializations/claude-code/README.md +99 -0
- package/specializations/claude-code/commands/doctor.md +31 -0
- package/specializations/claude-code/commands/init-memory.md +154 -0
- package/specializations/claude-code/commands/init-team.md +415 -0
- package/specializations/claude-code/commands/journal.md +66 -0
- package/specializations/claude-code/commands/review-prs.md +119 -0
- package/specializations/claude-code/hooks/check-global-state.sh +20 -0
- package/specializations/claude-code/hooks/session-end.sh +36 -0
- package/specializations/claude-code/settings.json +15 -0
- package/specializations/cursor/README.md +120 -0
- package/specializations/cursor/global-rule.mdc +53 -0
- package/specializations/cursor/repo-rule.mdc +25 -0
- package/specializations/generic/README.md +47 -0
- package/templates/autopilot/design.md +22 -0
- package/templates/autopilot/engineering.md +22 -0
- package/templates/autopilot/product.md +22 -0
- package/templates/autopilot/strategy.md +22 -0
- package/templates/autopilot/unified.md +24 -0
- package/templates/deploy/.env.example +110 -0
- package/templates/deploy/docker-compose.yml +63 -0
- package/templates/deploy/slack-app-manifest.json +45 -0
- package/templates/github-actions/meridian-digest.yml +85 -0
- package/templates/global.md +79 -0
- package/templates/memory-file.md +18 -0
- package/templates/personal-state.md +14 -0
- package/templates/personas.json +28 -0
- package/templates/product-state.md +41 -0
- package/templates/prompts-readme.md +19 -0
- package/templates/repo-state.md +18 -0
- package/templates/session-protocol-fragment.md +46 -0
- package/templates/slack-app-manifest.json +27 -0
- package/templates/statusline.sh +22 -0
- package/templates/strategy-state.md +39 -0
- package/templates/team-state.md +55 -0
- package/uninstall.sh +105 -0
- package/README.md +0 -4
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { execFile } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_TOKENS = 2048;
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 60000;
|
|
13
|
+
const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
14
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
15
|
+
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
16
|
+
|
|
17
|
+
// ── Simulation mode ─────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function isSimulation() {
|
|
20
|
+
return process.env.TEAM_CONTEXT_SIMULATE === '1';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getRepoRoot() {
|
|
24
|
+
// Walk up from this file's directory to find the repo root
|
|
25
|
+
let dir = __dirname;
|
|
26
|
+
for (let i = 0; i < 10; i++) {
|
|
27
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
28
|
+
const parent = path.dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
return process.cwd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadDigestFixture(personaId) {
|
|
36
|
+
const fixturesDir = process.env.TEAM_CONTEXT_SIM_DIGEST_DIR
|
|
37
|
+
|| path.join(getRepoRoot(), 'simulation', 'fixtures', 'digests');
|
|
38
|
+
|
|
39
|
+
const fixturePath = path.join(fixturesDir, `${personaId}.md`);
|
|
40
|
+
try {
|
|
41
|
+
return fs.readFileSync(fixturePath, 'utf8');
|
|
42
|
+
} catch {
|
|
43
|
+
return `[Simulated digest for ${personaId}]`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Make an HTTP/HTTPS POST request using Node.js built-ins.
|
|
51
|
+
* @param {string} url - Full URL to POST to
|
|
52
|
+
* @param {Object} headers - Request headers
|
|
53
|
+
* @param {string} body - JSON string body
|
|
54
|
+
* @returns {Promise<{ statusCode: number, headers: Object, body: string }>}
|
|
55
|
+
*/
|
|
56
|
+
function httpPost(url, headers, body) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
const transport = parsed.protocol === 'http:' ? http : https;
|
|
60
|
+
|
|
61
|
+
// Wall-clock deadline covers entire lifecycle (connect + response body)
|
|
62
|
+
const deadline = setTimeout(() => {
|
|
63
|
+
req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
64
|
+
}, REQUEST_TIMEOUT_MS);
|
|
65
|
+
|
|
66
|
+
const opts = {
|
|
67
|
+
hostname: parsed.hostname,
|
|
68
|
+
port: parsed.port || (parsed.protocol === 'http:' ? 80 : 443),
|
|
69
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
...headers,
|
|
73
|
+
'content-type': 'application/json',
|
|
74
|
+
'content-length': Buffer.byteLength(body),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const req = transport.request(opts, (res) => {
|
|
79
|
+
const chunks = [];
|
|
80
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
clearTimeout(deadline);
|
|
83
|
+
resolve({
|
|
84
|
+
statusCode: res.statusCode,
|
|
85
|
+
headers: res.headers,
|
|
86
|
+
body: Buffer.concat(chunks).toString(),
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
res.on('error', (err) => {
|
|
90
|
+
clearTimeout(deadline);
|
|
91
|
+
reject(new Error(`Response read error: ${err.message}`));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
req.on('error', (err) => {
|
|
96
|
+
clearTimeout(deadline);
|
|
97
|
+
reject(new Error(`Network error: ${err.message}`));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
req.write(body);
|
|
101
|
+
req.end();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check an HTTP response for common error conditions and throw descriptive errors.
|
|
107
|
+
* @param {Object} res - Response from httpPost
|
|
108
|
+
* @param {string} provider - Provider name for error messages
|
|
109
|
+
*/
|
|
110
|
+
function checkResponse(res, provider) {
|
|
111
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
112
|
+
throw new Error(`${provider}: Authentication failed (${res.statusCode}). Check your API key.`);
|
|
113
|
+
}
|
|
114
|
+
if (res.statusCode === 429) {
|
|
115
|
+
const retryAfter = res.headers['retry-after'];
|
|
116
|
+
const msg = retryAfter
|
|
117
|
+
? `${provider}: Rate limited. Retry after ${retryAfter} seconds.`
|
|
118
|
+
: `${provider}: Rate limited. Try again later.`;
|
|
119
|
+
throw new Error(msg);
|
|
120
|
+
}
|
|
121
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
122
|
+
const snippet = (res.body || '').slice(0, 500);
|
|
123
|
+
throw new Error(`${provider}: API returned ${res.statusCode}: ${snippet}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Provider: Anthropic ──────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function callAnthropic(config, systemPrompt, userContent) {
|
|
130
|
+
const apiKey = process.env[config.api_key_env];
|
|
131
|
+
if (!apiKey) {
|
|
132
|
+
throw new Error(`Anthropic: Missing API key. Set ${config.api_key_env} environment variable.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const payload = JSON.stringify({
|
|
136
|
+
model: config.model,
|
|
137
|
+
max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
|
|
138
|
+
system: systemPrompt,
|
|
139
|
+
messages: [{ role: 'user', content: userContent }],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const headers = {
|
|
143
|
+
'x-api-key': apiKey,
|
|
144
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const res = await httpPost(ANTHROPIC_API_URL, headers, payload);
|
|
148
|
+
checkResponse(res, 'Anthropic');
|
|
149
|
+
|
|
150
|
+
let data;
|
|
151
|
+
try {
|
|
152
|
+
data = JSON.parse(res.body);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error('Anthropic: Failed to parse response JSON.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!data.content || !Array.isArray(data.content) || data.content.length === 0) {
|
|
158
|
+
throw new Error('Anthropic: Response missing content array.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const textBlock = data.content.find((b) => b.type === 'text');
|
|
162
|
+
if (!textBlock) {
|
|
163
|
+
throw new Error('Anthropic: Response contains no text content block.');
|
|
164
|
+
}
|
|
165
|
+
return textBlock.text;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Provider: OpenAI-compatible ──────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async function callOpenAI(config, systemPrompt, userContent) {
|
|
171
|
+
const baseUrl = config.base_url || OPENAI_API_URL.replace('/chat/completions', '');
|
|
172
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
173
|
+
|
|
174
|
+
const headers = {};
|
|
175
|
+
if (config.api_key_env) {
|
|
176
|
+
const apiKey = process.env[config.api_key_env];
|
|
177
|
+
if (!apiKey) {
|
|
178
|
+
throw new Error(`OpenAI: Missing API key. Set ${config.api_key_env} environment variable.`);
|
|
179
|
+
}
|
|
180
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const payload = JSON.stringify({
|
|
184
|
+
model: config.model,
|
|
185
|
+
max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
|
|
186
|
+
messages: [
|
|
187
|
+
{ role: 'system', content: systemPrompt },
|
|
188
|
+
{ role: 'user', content: userContent },
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const res = await httpPost(url, headers, payload);
|
|
193
|
+
checkResponse(res, 'OpenAI');
|
|
194
|
+
|
|
195
|
+
let data;
|
|
196
|
+
try {
|
|
197
|
+
data = JSON.parse(res.body);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new Error('OpenAI: Failed to parse response JSON.');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
|
|
203
|
+
throw new Error('OpenAI: Response missing choices array.');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!data.choices[0].message || !data.choices[0].message.content) {
|
|
207
|
+
throw new Error('OpenAI: Response missing message content.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return data.choices[0].message.content;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Provider: CLI ────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
async function callCLI(config, systemPrompt, userContent) {
|
|
216
|
+
const command = config.command;
|
|
217
|
+
if (!command) {
|
|
218
|
+
throw new Error('CLI: No command configured. Set config.command (e.g. "ollama run llama3.2").');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Split command on first space: binary + remaining args
|
|
222
|
+
const spaceIdx = command.indexOf(' ');
|
|
223
|
+
const binary = spaceIdx === -1 ? command : command.slice(0, spaceIdx);
|
|
224
|
+
const args = spaceIdx === -1 ? [] : command.slice(spaceIdx + 1).split(/\s+/).filter(Boolean);
|
|
225
|
+
|
|
226
|
+
const input = systemPrompt + '\n---\n' + userContent;
|
|
227
|
+
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const proc = execFile(binary, args, {
|
|
230
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
231
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
232
|
+
}, (err, stdout, stderr) => {
|
|
233
|
+
if (err) {
|
|
234
|
+
if (err.killed) {
|
|
235
|
+
reject(new Error(`CLI: Command timed out after ${REQUEST_TIMEOUT_MS}ms.`));
|
|
236
|
+
} else {
|
|
237
|
+
reject(new Error(`CLI: Command failed: ${stderr || err.message}`));
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const output = stdout.trim();
|
|
243
|
+
if (!output) {
|
|
244
|
+
reject(new Error('CLI: Command produced no output.'));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
resolve(output);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Pipe input to stdin
|
|
252
|
+
if (proc.stdin) {
|
|
253
|
+
proc.stdin.write(input);
|
|
254
|
+
proc.stdin.end();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Main call() function ─────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Call an LLM provider with a system prompt and user content.
|
|
263
|
+
* @param {Object} config - Provider configuration
|
|
264
|
+
* @param {string} config.provider - 'anthropic' | 'openai' | 'cli' | 'simulate'
|
|
265
|
+
* @param {string} config.model - Model identifier
|
|
266
|
+
* @param {string} [config.api_key_env] - Environment variable name for API key
|
|
267
|
+
* @param {string} [config.base_url] - Override URL for OpenAI-compatible providers
|
|
268
|
+
* @param {string} [config.command] - CLI command for 'cli' provider
|
|
269
|
+
* @param {string} [config._personaId] - Persona ID for simulation mode
|
|
270
|
+
* @param {string} systemPrompt - System prompt
|
|
271
|
+
* @param {string} userContent - User content
|
|
272
|
+
* @returns {Promise<string>} - LLM response text
|
|
273
|
+
*/
|
|
274
|
+
async function call(config, systemPrompt, userContent) {
|
|
275
|
+
// Simulation mode overrides any provider setting
|
|
276
|
+
if (isSimulation() || config.provider === 'simulate') {
|
|
277
|
+
const personaId = config._personaId || 'engineering';
|
|
278
|
+
return loadDigestFixture(personaId);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
switch (config.provider) {
|
|
282
|
+
case 'anthropic':
|
|
283
|
+
return callAnthropic(config, systemPrompt, userContent);
|
|
284
|
+
case 'openai':
|
|
285
|
+
return callOpenAI(config, systemPrompt, userContent);
|
|
286
|
+
case 'cli':
|
|
287
|
+
return callCLI(config, systemPrompt, userContent);
|
|
288
|
+
default:
|
|
289
|
+
throw new Error(`Unknown LLM provider: "${config.provider}". Expected: anthropic, openai, cli, simulate.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Auto-detect available provider ───────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Detect the best available LLM provider from environment.
|
|
297
|
+
* @returns {Promise<{ provider: string, model: string, api_key_env: string|null, base_url: string|null, command: string|null }|null>}
|
|
298
|
+
*/
|
|
299
|
+
async function detect() {
|
|
300
|
+
// 1. Check for Anthropic API key
|
|
301
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
302
|
+
return {
|
|
303
|
+
provider: 'anthropic',
|
|
304
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
305
|
+
api_key_env: 'ANTHROPIC_API_KEY',
|
|
306
|
+
base_url: null,
|
|
307
|
+
command: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 2. Check for OpenAI API key
|
|
312
|
+
if (process.env.OPENAI_API_KEY) {
|
|
313
|
+
return {
|
|
314
|
+
provider: 'openai',
|
|
315
|
+
model: 'gpt-4o-mini',
|
|
316
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
317
|
+
base_url: null,
|
|
318
|
+
command: null,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 3. Check if ollama binary is available
|
|
323
|
+
const ollamaAvailable = await new Promise((resolve) => {
|
|
324
|
+
execFile('which', ['ollama'], (err) => {
|
|
325
|
+
resolve(!err);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (ollamaAvailable) {
|
|
330
|
+
return {
|
|
331
|
+
provider: 'openai',
|
|
332
|
+
model: 'llama3.2',
|
|
333
|
+
api_key_env: null,
|
|
334
|
+
base_url: 'http://localhost:11434/v1',
|
|
335
|
+
command: null,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 4. Nothing found
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Embeddings ────────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
const OPENAI_EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
|
|
346
|
+
const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small';
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate an embedding vector for the given text.
|
|
350
|
+
* Uses the OpenAI embeddings API (works with any OpenAI-compatible endpoint).
|
|
351
|
+
* @param {string} text - Text to embed
|
|
352
|
+
* @param {Object} [options] - Optional configuration
|
|
353
|
+
* @param {string} [options.model] - Embedding model (default: text-embedding-3-small)
|
|
354
|
+
* @param {string} [options.apiKeyEnv] - Env var name for API key (default: OPENAI_API_KEY)
|
|
355
|
+
* @param {string} [options.baseUrl] - Override base URL for OpenAI-compatible endpoints
|
|
356
|
+
* @returns {Promise<number[]>} - Embedding vector
|
|
357
|
+
*/
|
|
358
|
+
async function generateEmbedding(text, options = {}) {
|
|
359
|
+
// Simulation mode: return a deterministic fake vector
|
|
360
|
+
if (isSimulation()) {
|
|
361
|
+
const dim = 1536;
|
|
362
|
+
const vec = [];
|
|
363
|
+
let seed = 0;
|
|
364
|
+
for (let i = 0; i < text.length; i++) seed = ((seed << 5) - seed + text.charCodeAt(i)) | 0;
|
|
365
|
+
for (let i = 0; i < dim; i++) {
|
|
366
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
367
|
+
vec.push((seed / 0x7fffffff) * 2 - 1);
|
|
368
|
+
}
|
|
369
|
+
// Normalize
|
|
370
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
|
|
371
|
+
return vec.map(v => v / mag);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Azure OpenAI: uses different URL pattern and api-key header
|
|
375
|
+
const isAzure = !!(options.azureEndpoint || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
|
|
376
|
+
if (isAzure) {
|
|
377
|
+
return generateEmbeddingAzure(text, options);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const apiKeyEnv = options.apiKeyEnv || 'OPENAI_API_KEY';
|
|
381
|
+
const apiKey = process.env[apiKeyEnv];
|
|
382
|
+
if (!apiKey) {
|
|
383
|
+
throw new Error(`Embeddings: Missing API key. Set ${apiKeyEnv} environment variable.`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const baseUrl = options.baseUrl || OPENAI_EMBEDDINGS_URL.replace('/embeddings', '');
|
|
387
|
+
const url = `${baseUrl.replace(/\/+$/, '')}/embeddings`;
|
|
388
|
+
const model = options.model || DEFAULT_EMBEDDING_MODEL;
|
|
389
|
+
|
|
390
|
+
const payload = JSON.stringify({
|
|
391
|
+
model,
|
|
392
|
+
input: text,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const headers = {
|
|
396
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const res = await httpPost(url, headers, payload);
|
|
400
|
+
checkResponse(res, 'Embeddings');
|
|
401
|
+
|
|
402
|
+
let data;
|
|
403
|
+
try {
|
|
404
|
+
data = JSON.parse(res.body);
|
|
405
|
+
} catch {
|
|
406
|
+
throw new Error('Embeddings: Failed to parse response JSON.');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
|
|
410
|
+
throw new Error('Embeddings: Response missing data array.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!data.data[0].embedding || !Array.isArray(data.data[0].embedding)) {
|
|
414
|
+
throw new Error('Embeddings: Response missing embedding vector.');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return data.data[0].embedding;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Generate embedding via Azure OpenAI.
|
|
422
|
+
* URL pattern: {endpoint}/openai/deployments/{deployment}/embeddings?api-version=2024-02-01
|
|
423
|
+
* Auth: api-key header
|
|
424
|
+
*/
|
|
425
|
+
async function generateEmbeddingAzure(text, options = {}) {
|
|
426
|
+
const endpoint = (options.azureEndpoint || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT || '').replace(/\/+$/, '');
|
|
427
|
+
const apiKeyEnv = options.azureApiKeyEnv || 'AZURE_OPENAI_EMBEDDING_KEY';
|
|
428
|
+
const apiKey = process.env[apiKeyEnv];
|
|
429
|
+
const deployment = options.azureDeployment || process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT || 'text-embedding-3-small';
|
|
430
|
+
const apiVersion = options.azureApiVersion || '2024-02-01';
|
|
431
|
+
|
|
432
|
+
if (!endpoint) {
|
|
433
|
+
throw new Error('Azure OpenAI Embeddings: Set AZURE_OPENAI_EMBEDDING_ENDPOINT environment variable.');
|
|
434
|
+
}
|
|
435
|
+
if (!apiKey) {
|
|
436
|
+
throw new Error(`Azure OpenAI Embeddings: Set ${apiKeyEnv} environment variable.`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const url = `${endpoint}/openai/deployments/${deployment}/embeddings?api-version=${apiVersion}`;
|
|
440
|
+
const payload = JSON.stringify({ input: text });
|
|
441
|
+
const headers = { 'api-key': apiKey };
|
|
442
|
+
|
|
443
|
+
const res = await httpPost(url, headers, payload);
|
|
444
|
+
checkResponse(res, 'Azure OpenAI Embeddings');
|
|
445
|
+
|
|
446
|
+
let data;
|
|
447
|
+
try {
|
|
448
|
+
data = JSON.parse(res.body);
|
|
449
|
+
} catch {
|
|
450
|
+
throw new Error('Azure OpenAI Embeddings: Failed to parse response JSON.');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
|
|
454
|
+
throw new Error('Azure OpenAI Embeddings: Response missing data array.');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!data.data[0].embedding || !Array.isArray(data.data[0].embedding)) {
|
|
458
|
+
throw new Error('Azure OpenAI Embeddings: Response missing embedding vector.');
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return data.data[0].embedding;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = {
|
|
465
|
+
call,
|
|
466
|
+
detect,
|
|
467
|
+
generateEmbedding,
|
|
468
|
+
isSimulation,
|
|
469
|
+
};
|