neoagent 1.4.0 → 1.4.3
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/.env.example +5 -0
- package/com.neoagent.plist +8 -6
- package/docs/configuration.md +9 -1
- package/docs/skills.md +6 -2
- package/lib/manager.js +37 -10
- package/package.json +4 -1
- package/runtime/paths.js +80 -0
- package/server/db/database.js +78 -4
- package/server/index.js +5 -5
- package/server/public/app.html +124 -49
- package/server/public/assets/world-office-dark.png +0 -0
- package/server/public/assets/world-office-light.png +0 -0
- package/server/public/css/app.css +575 -242
- package/server/public/css/styles.css +445 -121
- package/server/public/js/app.js +1041 -423
- package/server/routes/memory.js +3 -1
- package/server/routes/settings.js +42 -6
- package/server/routes/skills.js +124 -84
- package/server/routes/store.js +102 -1
- package/server/services/ai/compaction.js +15 -31
- package/server/services/ai/engine.js +224 -202
- package/server/services/ai/history.js +188 -0
- package/server/services/ai/learning.js +143 -0
- package/server/services/ai/providers/google.js +8 -1
- package/server/services/ai/settings.js +80 -0
- package/server/services/ai/systemPrompt.js +57 -98
- package/server/services/ai/toolResult.js +151 -0
- package/server/services/ai/toolRunner.js +26 -7
- package/server/services/ai/toolSelector.js +140 -0
- package/server/services/ai/tools.js +158 -5
- package/server/services/browser/controller.js +124 -48
- package/server/services/manager.js +26 -3
- package/server/services/mcp/client.js +1 -1
- package/server/services/memory/embeddings.js +80 -14
- package/server/services/memory/manager.js +211 -17
- package/server/services/messaging/telnyx.js +3 -2
- package/server/services/messaging/whatsapp.js +3 -2
- package/server/services/scheduler/cron.js +6 -1
- package/server/services/websocket.js +19 -6
|
@@ -1,36 +1,118 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const { DATA_DIR } = require('../../../runtime/paths');
|
|
3
4
|
|
|
4
|
-
const SCREENSHOTS_DIR = path.join(
|
|
5
|
+
const SCREENSHOTS_DIR = path.join(DATA_DIR, 'screenshots');
|
|
5
6
|
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
6
7
|
|
|
8
|
+
const USER_AGENTS = [
|
|
9
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
10
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
11
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
12
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
13
|
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const VIEWPORTS = [
|
|
17
|
+
{ width: 1280, height: 800 },
|
|
18
|
+
{ width: 1366, height: 768 },
|
|
19
|
+
{ width: 1440, height: 900 },
|
|
20
|
+
{ width: 1920, height: 1080 },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function rand(min, max) {
|
|
24
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise(r => setTimeout(r, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
class BrowserController {
|
|
8
32
|
constructor(io) {
|
|
9
33
|
this.io = io;
|
|
10
34
|
this.browser = null;
|
|
11
35
|
this.page = null;
|
|
12
36
|
this.launching = false;
|
|
13
|
-
this.headless = true;
|
|
37
|
+
this.headless = true;
|
|
38
|
+
this._viewport = VIEWPORTS[0];
|
|
39
|
+
this._userAgent = USER_AGENTS[0];
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
async setHeadless(val) {
|
|
17
43
|
const wasHeadless = this.headless;
|
|
18
44
|
this.headless = val !== false && val !== 'false';
|
|
19
|
-
// Close browser so it relaunches with new setting next time
|
|
20
45
|
if (wasHeadless !== this.headless) {
|
|
21
46
|
await this.close().catch(() => { });
|
|
22
47
|
}
|
|
23
48
|
}
|
|
24
49
|
|
|
25
|
-
// Alias used by graceful shutdown in server.js
|
|
26
50
|
async closeBrowser() {
|
|
27
51
|
return this.close();
|
|
28
52
|
}
|
|
29
53
|
|
|
54
|
+
async _applyStealthToPage(page) {
|
|
55
|
+
const ua = this._userAgent;
|
|
56
|
+
const vp = this._viewport;
|
|
57
|
+
|
|
58
|
+
await page.setUserAgent(ua);
|
|
59
|
+
await page.setViewport(vp);
|
|
60
|
+
await page.setExtraHTTPHeaders({
|
|
61
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Inject fingerprint overrides before any page script runs
|
|
65
|
+
await page.evaluateOnNewDocument(`
|
|
66
|
+
(() => {
|
|
67
|
+
// Remove webdriver flag
|
|
68
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
69
|
+
|
|
70
|
+
// Realistic language/platform
|
|
71
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
72
|
+
Object.defineProperty(navigator, 'platform', { get: () => 'MacIntel' });
|
|
73
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => ${rand(4, 16)} });
|
|
74
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: () => ${[4, 8, 16][rand(0, 2)]} });
|
|
75
|
+
|
|
76
|
+
// Make it look like a real Chrome install
|
|
77
|
+
window.chrome = {
|
|
78
|
+
app: { isInstalled: false, InstallState: {}, RunningState: {} },
|
|
79
|
+
runtime: {},
|
|
80
|
+
loadTimes: function() {},
|
|
81
|
+
csi: function() {},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Permissions API — bots often show "denied" for notifications
|
|
85
|
+
const origQuery = window.navigator.permissions?.query?.bind(navigator.permissions);
|
|
86
|
+
if (origQuery) {
|
|
87
|
+
navigator.permissions.query = (parameters) =>
|
|
88
|
+
parameters.name === 'notifications'
|
|
89
|
+
? Promise.resolve({ state: Notification.permission })
|
|
90
|
+
: origQuery(parameters);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Hide automation plugins gap
|
|
94
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
95
|
+
get: () => {
|
|
96
|
+
const arr = [
|
|
97
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
98
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
99
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
100
|
+
];
|
|
101
|
+
arr.item = i => arr[i];
|
|
102
|
+
arr.namedItem = n => arr.find(p => p.name === n) || null;
|
|
103
|
+
arr.refresh = () => {};
|
|
104
|
+
Object.defineProperty(arr, 'length', { get: () => arr.length });
|
|
105
|
+
return arr;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
})();
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
|
|
30
112
|
async ensureBrowser() {
|
|
31
113
|
if (this.browser && this.browser.isConnected()) return;
|
|
32
114
|
if (this.launching) {
|
|
33
|
-
await
|
|
115
|
+
await sleep(2000);
|
|
34
116
|
return;
|
|
35
117
|
}
|
|
36
118
|
|
|
@@ -40,29 +122,28 @@ class BrowserController {
|
|
|
40
122
|
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
|
41
123
|
puppeteer.use(StealthPlugin());
|
|
42
124
|
|
|
125
|
+
this._userAgent = USER_AGENTS[rand(0, USER_AGENTS.length - 1)];
|
|
126
|
+
this._viewport = VIEWPORTS[rand(0, VIEWPORTS.length - 1)];
|
|
127
|
+
|
|
43
128
|
this.browser = await puppeteer.launch({
|
|
44
129
|
headless: this.headless ? 'new' : false,
|
|
45
130
|
args: [
|
|
46
131
|
'--no-sandbox',
|
|
47
132
|
'--disable-setuid-sandbox',
|
|
48
133
|
'--disable-dev-shm-usage',
|
|
49
|
-
'--disable-
|
|
50
|
-
'--
|
|
134
|
+
'--disable-blink-features=AutomationControlled',
|
|
135
|
+
'--disable-infobars',
|
|
136
|
+
'--no-first-run',
|
|
137
|
+
'--no-default-browser-check',
|
|
138
|
+
'--lang=en-US,en',
|
|
139
|
+
`--window-size=${this._viewport.width},${this._viewport.height}`,
|
|
51
140
|
],
|
|
52
|
-
defaultViewport:
|
|
141
|
+
defaultViewport: this._viewport,
|
|
142
|
+
ignoreDefaultArgs: ['--enable-automation'],
|
|
53
143
|
});
|
|
54
|
-
this.page = await this.browser.newPage();
|
|
55
144
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
|
59
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0',
|
|
60
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0',
|
|
61
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15'
|
|
62
|
-
];
|
|
63
|
-
const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
|
|
64
|
-
await this.page.setUserAgent(randomUA);
|
|
65
|
-
this._currentUserAgent = randomUA;
|
|
145
|
+
this.page = await this.browser.newPage();
|
|
146
|
+
await this._applyStealthToPage(this.page);
|
|
66
147
|
} finally {
|
|
67
148
|
this.launching = false;
|
|
68
149
|
}
|
|
@@ -72,9 +153,7 @@ class BrowserController {
|
|
|
72
153
|
await this.ensureBrowser();
|
|
73
154
|
if (!this.page || this.page.isClosed()) {
|
|
74
155
|
this.page = await this.browser.newPage();
|
|
75
|
-
|
|
76
|
-
await this.page.setUserAgent(this._currentUserAgent);
|
|
77
|
-
}
|
|
156
|
+
await this._applyStealthToPage(this.page);
|
|
78
157
|
}
|
|
79
158
|
return this.page;
|
|
80
159
|
}
|
|
@@ -113,6 +192,9 @@ class BrowserController {
|
|
|
113
192
|
await page.waitForSelector(options.waitFor, { timeout: 10000 }).catch(() => { });
|
|
114
193
|
}
|
|
115
194
|
|
|
195
|
+
// Simulate human reading delay
|
|
196
|
+
await sleep(rand(500, 1500));
|
|
197
|
+
|
|
116
198
|
const title = await page.title();
|
|
117
199
|
const currentUrl = page.url();
|
|
118
200
|
|
|
@@ -125,8 +207,7 @@ class BrowserController {
|
|
|
125
207
|
const body = document.body;
|
|
126
208
|
if (!body) return '';
|
|
127
209
|
const clone = body.cloneNode(true);
|
|
128
|
-
|
|
129
|
-
scripts.forEach(s => s.remove());
|
|
210
|
+
clone.querySelectorAll('script, style, noscript').forEach(s => s.remove());
|
|
130
211
|
return clone.innerText.slice(0, 10000);
|
|
131
212
|
});
|
|
132
213
|
|
|
@@ -152,40 +233,39 @@ class BrowserController {
|
|
|
152
233
|
const page = await this.ensurePage();
|
|
153
234
|
|
|
154
235
|
try {
|
|
236
|
+
let target = null;
|
|
237
|
+
|
|
155
238
|
if (text && !selector) {
|
|
156
239
|
const elements = await page.$$('a, button, [role="button"], input[type="submit"], [onclick]');
|
|
157
|
-
let found = false;
|
|
158
240
|
for (const el of elements) {
|
|
159
241
|
const elText = await page.evaluate(e => e.innerText || e.value || e.getAttribute('aria-label') || '', el);
|
|
160
242
|
if (elText.toLowerCase().includes(text.toLowerCase())) {
|
|
161
|
-
|
|
162
|
-
found = true;
|
|
243
|
+
target = el;
|
|
163
244
|
break;
|
|
164
245
|
}
|
|
165
246
|
}
|
|
166
|
-
if (!
|
|
167
|
-
return { error: `No clickable element found with text: ${text}` };
|
|
168
|
-
}
|
|
247
|
+
if (!target) return { error: `No clickable element found with text: ${text}` };
|
|
169
248
|
} else if (selector) {
|
|
170
|
-
await page
|
|
249
|
+
target = await page.$(selector);
|
|
250
|
+
if (!target) return { error: `Element not found: ${selector}` };
|
|
171
251
|
} else {
|
|
172
252
|
return { error: 'Either selector or text required' };
|
|
173
253
|
}
|
|
174
254
|
|
|
175
|
-
|
|
255
|
+
// Human-like: hover first, then click with a hold delay
|
|
256
|
+
await target.hover();
|
|
257
|
+
await sleep(rand(80, 250));
|
|
258
|
+
await target.click({ delay: rand(50, 150) });
|
|
176
259
|
|
|
177
|
-
|
|
178
|
-
if (screenshot) {
|
|
179
|
-
screenshotResult = await this.takeScreenshot();
|
|
180
|
-
}
|
|
260
|
+
await sleep(rand(800, 1800));
|
|
181
261
|
|
|
182
|
-
|
|
183
|
-
|
|
262
|
+
let screenshotResult = null;
|
|
263
|
+
if (screenshot) screenshotResult = await this.takeScreenshot();
|
|
184
264
|
|
|
185
265
|
return {
|
|
186
266
|
success: true,
|
|
187
|
-
url:
|
|
188
|
-
title,
|
|
267
|
+
url: page.url(),
|
|
268
|
+
title: await page.title(),
|
|
189
269
|
screenshotPath: screenshotResult?.screenshotPath || null
|
|
190
270
|
};
|
|
191
271
|
} catch (err) {
|
|
@@ -202,21 +282,17 @@ class BrowserController {
|
|
|
202
282
|
await page.keyboard.press('Backspace');
|
|
203
283
|
}
|
|
204
284
|
|
|
205
|
-
// Simulate human typing speeds (between 30ms and 150ms per keystroke)
|
|
206
285
|
for (const char of text) {
|
|
207
|
-
|
|
208
|
-
await page.type(selector, char, { delay: charDelay });
|
|
286
|
+
await page.type(selector, char, { delay: rand(30, 150) });
|
|
209
287
|
}
|
|
210
288
|
|
|
211
289
|
if (options.pressEnter) {
|
|
212
290
|
await page.keyboard.press('Enter');
|
|
213
|
-
await
|
|
291
|
+
await sleep(1000);
|
|
214
292
|
}
|
|
215
293
|
|
|
216
294
|
let screenshotResult = null;
|
|
217
|
-
if (options.screenshot !== false)
|
|
218
|
-
screenshotResult = await this.takeScreenshot();
|
|
219
|
-
}
|
|
295
|
+
if (options.screenshot !== false) screenshotResult = await this.takeScreenshot();
|
|
220
296
|
|
|
221
297
|
return {
|
|
222
298
|
success: true,
|
|
@@ -268,7 +344,7 @@ class BrowserController {
|
|
|
268
344
|
}
|
|
269
345
|
|
|
270
346
|
async screenshot(options = {}) {
|
|
271
|
-
return
|
|
347
|
+
return this.takeScreenshot(options);
|
|
272
348
|
}
|
|
273
349
|
|
|
274
350
|
async launch(options = {}) {
|
|
@@ -5,7 +5,9 @@ const { MemoryManager } = require('./memory/manager');
|
|
|
5
5
|
const { MCPClient } = require('./mcp/client');
|
|
6
6
|
const { BrowserController } = require('./browser/controller');
|
|
7
7
|
const { AgentEngine } = require('./ai/engine');
|
|
8
|
+
const { LearningManager } = require('./ai/learning');
|
|
8
9
|
const { MultiStepOrchestrator } = require('./ai/multiStep');
|
|
10
|
+
const { SkillRunner } = require('./ai/toolRunner');
|
|
9
11
|
const { MessagingManager } = require('./messaging/manager');
|
|
10
12
|
const { Scheduler } = require('./scheduler/cron');
|
|
11
13
|
const { setupWebSocket } = require('./websocket');
|
|
@@ -29,7 +31,21 @@ async function startServices(app, io) {
|
|
|
29
31
|
}
|
|
30
32
|
app.locals.browserController = browserController;
|
|
31
33
|
|
|
32
|
-
const
|
|
34
|
+
const skillRunner = new SkillRunner();
|
|
35
|
+
await skillRunner.loadSkills();
|
|
36
|
+
app.locals.skillRunner = skillRunner;
|
|
37
|
+
|
|
38
|
+
const learningManager = new LearningManager(skillRunner, io);
|
|
39
|
+
app.locals.learningManager = learningManager;
|
|
40
|
+
|
|
41
|
+
const agentEngine = new AgentEngine(io, {
|
|
42
|
+
memoryManager,
|
|
43
|
+
mcpClient,
|
|
44
|
+
browserController,
|
|
45
|
+
messagingManager: null,
|
|
46
|
+
skillRunner,
|
|
47
|
+
learningManager
|
|
48
|
+
});
|
|
33
49
|
app.locals.agentEngine = agentEngine;
|
|
34
50
|
|
|
35
51
|
const multiStep = new MultiStepOrchestrator(agentEngine, io);
|
|
@@ -162,12 +178,19 @@ async function startServices(app, io) {
|
|
|
162
178
|
await processMessage(userId, msg);
|
|
163
179
|
});
|
|
164
180
|
|
|
165
|
-
const scheduler = new Scheduler(io, agentEngine);
|
|
181
|
+
const scheduler = new Scheduler(io, agentEngine, app);
|
|
166
182
|
app.locals.scheduler = scheduler;
|
|
167
183
|
agentEngine.scheduler = scheduler;
|
|
168
184
|
scheduler.start();
|
|
169
185
|
|
|
170
|
-
setupWebSocket(io, {
|
|
186
|
+
setupWebSocket(io, {
|
|
187
|
+
agentEngine,
|
|
188
|
+
messagingManager,
|
|
189
|
+
mcpClient,
|
|
190
|
+
scheduler,
|
|
191
|
+
memoryManager,
|
|
192
|
+
app
|
|
193
|
+
});
|
|
171
194
|
app.locals.io = io;
|
|
172
195
|
|
|
173
196
|
console.log('All services initialized');
|
|
@@ -12,7 +12,7 @@ class DBAuthProvider {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
get redirectUrl() {
|
|
15
|
-
const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT ||
|
|
15
|
+
const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3333}`;
|
|
16
16
|
return `${baseUrl}/api/mcp/oauth/callback`;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -2,30 +2,80 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Embedding helpers for the semantic memory system.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Provider selection (in priority order):
|
|
7
|
+
* 1. Google (text-embedding-004, 768 dims) — when provider hint is 'google' and GOOGLE_AI_KEY is set
|
|
8
|
+
* 2. OpenAI (text-embedding-3-small, 1536 dims) — when OPENAI_API_KEY is set
|
|
9
|
+
* 3. Keyword fallback — when no API key is available
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
const https = require('https');
|
|
10
13
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const OPENAI_MODEL = 'text-embedding-3-small';
|
|
15
|
+
const OPENAI_DIM = 1536;
|
|
16
|
+
const GOOGLE_MODEL = 'text-embedding-004';
|
|
17
|
+
const GOOGLE_DIM = 768;
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
async function
|
|
19
|
+
// Exported so callers can sanity-check stored vector dimensions if needed
|
|
20
|
+
const EMBED_DIM = OPENAI_DIM;
|
|
21
|
+
const EMBED_DIM_GOOGLE = GOOGLE_DIM;
|
|
22
|
+
|
|
23
|
+
async function getGeminiEmbedding(text) {
|
|
24
|
+
const apiKey = process.env.GOOGLE_AI_KEY;
|
|
25
|
+
if (!apiKey) return null;
|
|
26
|
+
if (!text || !text.trim()) return null;
|
|
27
|
+
|
|
28
|
+
const truncated = text.slice(0, 25000);
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const body = JSON.stringify({
|
|
32
|
+
model: `models/${GOOGLE_MODEL}`,
|
|
33
|
+
content: { parts: [{ text: truncated }] }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const path = `/v1beta/models/${GOOGLE_MODEL}:embedContent?key=${apiKey}`;
|
|
37
|
+
const options = {
|
|
38
|
+
hostname: 'generativelanguage.googleapis.com',
|
|
39
|
+
path,
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
'Content-Length': Buffer.byteLength(body)
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = https.request(options, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', chunk => { data += chunk; });
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(data);
|
|
53
|
+
const vec = parsed.embedding?.values;
|
|
54
|
+
if (!vec) return resolve(null);
|
|
55
|
+
resolve(new Float32Array(vec));
|
|
56
|
+
} catch {
|
|
57
|
+
resolve(null);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
req.on('error', () => resolve(null));
|
|
63
|
+
req.setTimeout(15000, () => { req.destroy(); resolve(null); });
|
|
64
|
+
req.write(body);
|
|
65
|
+
req.end();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function getOpenAIEmbedding(text) {
|
|
19
70
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
20
71
|
if (!apiKey) return null;
|
|
21
72
|
if (!text || !text.trim()) return null;
|
|
22
73
|
|
|
23
|
-
// Truncate very long text to stay within token limits (~8k tokens)
|
|
24
74
|
const truncated = text.slice(0, 25000);
|
|
25
75
|
|
|
26
|
-
return new Promise((resolve
|
|
76
|
+
return new Promise((resolve) => {
|
|
27
77
|
const body = JSON.stringify({
|
|
28
|
-
model:
|
|
78
|
+
model: OPENAI_MODEL,
|
|
29
79
|
input: truncated,
|
|
30
80
|
encoding_format: 'float'
|
|
31
81
|
});
|
|
@@ -64,6 +114,21 @@ async function getEmbedding(text) {
|
|
|
64
114
|
});
|
|
65
115
|
}
|
|
66
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Get an embedding vector for a piece of text.
|
|
119
|
+
* @param {string} text
|
|
120
|
+
* @param {string} [provider] - 'google' to prefer Gemini embeddings
|
|
121
|
+
* @returns {Float32Array|null}
|
|
122
|
+
*/
|
|
123
|
+
async function getEmbedding(text, provider) {
|
|
124
|
+
if (!text || !text.trim()) return null;
|
|
125
|
+
if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
|
|
126
|
+
const vec = await getGeminiEmbedding(text);
|
|
127
|
+
if (vec) return vec;
|
|
128
|
+
}
|
|
129
|
+
return getOpenAIEmbedding(text);
|
|
130
|
+
}
|
|
131
|
+
|
|
67
132
|
/**
|
|
68
133
|
* Cosine similarity between two Float32Arrays.
|
|
69
134
|
* Returns a value in [-1, 1]; higher = more similar.
|
|
@@ -72,7 +137,7 @@ function cosineSimilarity(a, b) {
|
|
|
72
137
|
if (!a || !b || a.length !== b.length) return 0;
|
|
73
138
|
let dot = 0, magA = 0, magB = 0;
|
|
74
139
|
for (let i = 0; i < a.length; i++) {
|
|
75
|
-
dot
|
|
140
|
+
dot += a[i] * b[i];
|
|
76
141
|
magA += a[i] * a[i];
|
|
77
142
|
magB += b[i] * b[i];
|
|
78
143
|
}
|
|
@@ -122,5 +187,6 @@ module.exports = {
|
|
|
122
187
|
serializeEmbedding,
|
|
123
188
|
deserializeEmbedding,
|
|
124
189
|
keywordSimilarity,
|
|
125
|
-
EMBED_DIM
|
|
190
|
+
EMBED_DIM,
|
|
191
|
+
EMBED_DIM_GOOGLE
|
|
126
192
|
};
|