@dropout-ai/runtime 0.3.5 â 0.3.6
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/package.json +1 -1
- package/src/index.js +114 -27
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
|
-
* Universal AI Interaction Capture for Node.js
|
|
3
|
+
* Universal AI Interaction Capture for Node.js
|
|
4
4
|
* Patches: fetch, http.request, https.request
|
|
5
|
-
*
|
|
5
|
+
* Behavior: Splits interactions into separate User/Assistant rows for full analytics.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const https = require('https');
|
|
@@ -12,7 +12,7 @@ const crypto = require('crypto');
|
|
|
12
12
|
// --- DEFAULT CONFIGURATION ---
|
|
13
13
|
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
14
14
|
|
|
15
|
-
// Known AI Domains
|
|
15
|
+
// Known AI Domains
|
|
16
16
|
const KNOWN_AI_DOMAINS = [
|
|
17
17
|
'api.openai.com', 'api.anthropic.com', 'generativelanguage.googleapis.com',
|
|
18
18
|
'aiplatform.googleapis.com', 'api.groq.com', 'api.mistral.ai', 'api.cohere.ai'
|
|
@@ -21,8 +21,6 @@ const KNOWN_AI_DOMAINS = [
|
|
|
21
21
|
class Dropout {
|
|
22
22
|
constructor(config = {}) {
|
|
23
23
|
// đ¨ BROWSER GUARD đ¨
|
|
24
|
-
// If this runs in the browser (Client Component), do nothing.
|
|
25
|
-
// This prevents the "listener" error and protects your API keys.
|
|
26
24
|
if (typeof window !== 'undefined') {
|
|
27
25
|
console.warn("[Dropout] â ī¸ Initialization Skipped: This SDK is for Node.js/Server environments only.");
|
|
28
26
|
return;
|
|
@@ -34,32 +32,39 @@ class Dropout {
|
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
// 2. Config Setup
|
|
35
|
+
// 2. Config & State Setup
|
|
38
36
|
this.config = config;
|
|
39
37
|
this.projectId = config.projectId;
|
|
40
38
|
this.apiKey = config.apiKey;
|
|
41
39
|
this.debug = config.debug || false;
|
|
42
40
|
this.privacy = config.privacy || 'full';
|
|
43
41
|
this.captureEndpoint = SUPABASE_FUNCTION_URL;
|
|
42
|
+
this.maxOutputBytes = 32768;
|
|
44
43
|
|
|
45
|
-
// 3.
|
|
44
|
+
// 3. Conversation State (Matches your snippet)
|
|
45
|
+
this.turnIndex = 0;
|
|
46
|
+
this.lastTurnConfirmed = true;
|
|
47
|
+
this.lastPromptHash = null;
|
|
48
|
+
this.lastResponseHash = null;
|
|
49
|
+
|
|
50
|
+
// 4. Singleton Guard
|
|
46
51
|
if (global.__dropout_initialized__) {
|
|
47
52
|
if (this.debug) console.log("[Dropout] âšī¸ Already initialized. Skipping patch.");
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
55
|
global.__dropout_initialized__ = true;
|
|
51
56
|
|
|
52
|
-
//
|
|
57
|
+
// 5. Initialize Identity
|
|
53
58
|
if (!global.__dropout_session_id__) {
|
|
54
59
|
global.__dropout_session_id__ = this.generateSessionId();
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
//
|
|
62
|
+
// 6. Start
|
|
58
63
|
if (this.debug) console.log(`[Dropout] đĸ Initialized for Project: ${this.projectId}`);
|
|
59
64
|
|
|
60
65
|
// Bind methods
|
|
61
66
|
this.log = this.log.bind(this);
|
|
62
|
-
this.
|
|
67
|
+
this.emit = this.emit.bind(this);
|
|
63
68
|
this.patchNetwork();
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -102,6 +107,8 @@ class Dropout {
|
|
|
102
107
|
try {
|
|
103
108
|
const parsed = typeof body === 'string' ? JSON.parse(body) : body;
|
|
104
109
|
if (parsed.model) model = parsed.model;
|
|
110
|
+
// Simple heuristic fallback if model is missing but structure looks AI-ish
|
|
111
|
+
if (provider === 'custom' && (parsed.messages || parsed.prompt)) provider = 'heuristic';
|
|
105
112
|
} catch (e) { }
|
|
106
113
|
}
|
|
107
114
|
return { provider, model };
|
|
@@ -124,15 +131,29 @@ class Dropout {
|
|
|
124
131
|
emit(payload) {
|
|
125
132
|
if (!this.apiKey || !this.projectId) return;
|
|
126
133
|
|
|
134
|
+
// Construct Final Payload matching your Inbox Table
|
|
127
135
|
const finalPayload = {
|
|
128
|
-
...payload,
|
|
129
136
|
project_id: this.projectId,
|
|
130
|
-
|
|
137
|
+
session_id: payload.session_id,
|
|
138
|
+
turn_index: payload.turn_index,
|
|
139
|
+
turn_role: payload.turn_role,
|
|
140
|
+
|
|
141
|
+
// Data Columns
|
|
142
|
+
provider: payload.provider,
|
|
143
|
+
model: payload.model,
|
|
144
|
+
latency_ms: payload.latency_ms || null,
|
|
145
|
+
|
|
146
|
+
// Content Columns (Both naming conventions)
|
|
131
147
|
content: payload.content,
|
|
148
|
+
//content_blob: payload.content,
|
|
149
|
+
content_hash: payload.content_hash,
|
|
150
|
+
|
|
151
|
+
// Metadata
|
|
152
|
+
metadata_flags: payload.metadata_flags,
|
|
132
153
|
received_at: new Date().toISOString()
|
|
133
154
|
};
|
|
134
155
|
|
|
135
|
-
this.log(`đ Sending Capture
|
|
156
|
+
this.log(`đ Sending Capture [${payload.turn_role}]`);
|
|
136
157
|
|
|
137
158
|
const req = https.request(this.captureEndpoint, {
|
|
138
159
|
method: 'POST',
|
|
@@ -162,6 +183,7 @@ class Dropout {
|
|
|
162
183
|
try { bodyStr = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
163
184
|
}
|
|
164
185
|
|
|
186
|
+
// 1. Guard
|
|
165
187
|
if (!_this.isAiRequest(url, bodyStr)) {
|
|
166
188
|
return originalFetch.apply(this, arguments);
|
|
167
189
|
}
|
|
@@ -169,17 +191,36 @@ class Dropout {
|
|
|
169
191
|
_this.log(`⥠[FETCH] Intercepting: ${url}`);
|
|
170
192
|
const start = Date.now();
|
|
171
193
|
|
|
194
|
+
// 2. Determine Turn Logic (Your Snippet)
|
|
195
|
+
let activeTurn;
|
|
196
|
+
if (_this.lastTurnConfirmed) {
|
|
197
|
+
activeTurn = _this.turnIndex++;
|
|
198
|
+
_this.lastTurnConfirmed = false;
|
|
199
|
+
} else {
|
|
200
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
const { provider, model } = _this.normalize(url, bodyStr);
|
|
204
|
+
const pHash = _this.hash(bodyStr);
|
|
205
|
+
|
|
206
|
+
// 3. Emit Request (User)
|
|
173
207
|
_this.emit({
|
|
174
208
|
session_id: global.__dropout_session_id__,
|
|
175
|
-
turn_index:
|
|
209
|
+
turn_index: activeTurn,
|
|
176
210
|
turn_role: 'user',
|
|
177
211
|
provider,
|
|
178
212
|
model,
|
|
179
213
|
content: _this.privacy === 'full' ? bodyStr : null,
|
|
180
|
-
|
|
214
|
+
content_hash: pHash,
|
|
215
|
+
metadata_flags: {
|
|
216
|
+
retry_like: pHash === _this.lastPromptHash ? 1 : 0,
|
|
217
|
+
method: 'FETCH',
|
|
218
|
+
url: url
|
|
219
|
+
}
|
|
181
220
|
});
|
|
221
|
+
_this.lastPromptHash = pHash;
|
|
182
222
|
|
|
223
|
+
// 4. Actual Network Call
|
|
183
224
|
let response;
|
|
184
225
|
try {
|
|
185
226
|
response = await originalFetch.apply(this, arguments);
|
|
@@ -187,19 +228,32 @@ class Dropout {
|
|
|
187
228
|
|
|
188
229
|
const latency = Date.now() - start;
|
|
189
230
|
|
|
231
|
+
// 5. Emit Response (Assistant)
|
|
190
232
|
try {
|
|
191
233
|
const cloned = response.clone();
|
|
192
|
-
|
|
234
|
+
let oText = await cloned.text();
|
|
235
|
+
if (oText && oText.length > _this.maxOutputBytes) {
|
|
236
|
+
oText = oText.slice(0, _this.maxOutputBytes);
|
|
237
|
+
}
|
|
238
|
+
const oHash = _this.hash(oText);
|
|
239
|
+
|
|
193
240
|
_this.emit({
|
|
194
241
|
session_id: global.__dropout_session_id__,
|
|
195
|
-
turn_index:
|
|
242
|
+
turn_index: activeTurn,
|
|
196
243
|
turn_role: 'assistant',
|
|
197
244
|
latency_ms: latency,
|
|
198
245
|
provider,
|
|
199
246
|
model,
|
|
200
247
|
content: _this.privacy === 'full' ? oText : null,
|
|
201
|
-
|
|
248
|
+
content_hash: oHash,
|
|
249
|
+
metadata_flags: {
|
|
250
|
+
non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
|
|
251
|
+
turn_boundary_confirmed: 1,
|
|
252
|
+
status: response.status
|
|
253
|
+
}
|
|
202
254
|
});
|
|
255
|
+
_this.lastResponseHash = oHash;
|
|
256
|
+
_this.lastTurnConfirmed = true;
|
|
203
257
|
} catch (e) { _this.log("â ī¸ Failed to read response body"); }
|
|
204
258
|
|
|
205
259
|
return response;
|
|
@@ -234,6 +288,7 @@ class Dropout {
|
|
|
234
288
|
const originalWrite = clientRequest.write;
|
|
235
289
|
const originalEnd = clientRequest.end;
|
|
236
290
|
|
|
291
|
+
// Buffer Request Data
|
|
237
292
|
clientRequest.write = function (...writeArgs) {
|
|
238
293
|
const chunk = writeArgs[0];
|
|
239
294
|
if (chunk && (typeof chunk === 'string' || Buffer.isBuffer(chunk))) {
|
|
@@ -248,41 +303,73 @@ class Dropout {
|
|
|
248
303
|
reqChunks.push(Buffer.from(chunk));
|
|
249
304
|
}
|
|
250
305
|
|
|
306
|
+
// --- EMIT REQUEST (On End) ---
|
|
251
307
|
const reqBody = Buffer.concat(reqChunks).toString('utf8');
|
|
252
308
|
const { provider, model } = _this.normalize(url, reqBody);
|
|
253
|
-
|
|
309
|
+
const pHash = _this.hash(reqBody);
|
|
310
|
+
|
|
311
|
+
// Turn Logic
|
|
312
|
+
let activeTurn;
|
|
313
|
+
if (_this.lastTurnConfirmed) {
|
|
314
|
+
activeTurn = _this.turnIndex++;
|
|
315
|
+
_this.lastTurnConfirmed = false;
|
|
316
|
+
} else {
|
|
317
|
+
activeTurn = _this.turnIndex > 0 ? _this.turnIndex - 1 : 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Save state for response
|
|
321
|
+
clientRequest._dropout_turn = activeTurn;
|
|
322
|
+
clientRequest._dropout_provider = provider;
|
|
323
|
+
clientRequest._dropout_model = model;
|
|
254
324
|
|
|
255
325
|
_this.emit({
|
|
256
326
|
session_id: global.__dropout_session_id__,
|
|
257
|
-
turn_index:
|
|
327
|
+
turn_index: activeTurn,
|
|
258
328
|
turn_role: 'user',
|
|
259
329
|
provider,
|
|
260
330
|
model,
|
|
261
331
|
content: _this.privacy === 'full' ? reqBody : null,
|
|
262
|
-
|
|
332
|
+
content_hash: pHash,
|
|
333
|
+
metadata_flags: {
|
|
334
|
+
retry_like: pHash === _this.lastPromptHash ? 1 : 0,
|
|
335
|
+
method: moduleName.toUpperCase(),
|
|
336
|
+
url: url
|
|
337
|
+
}
|
|
263
338
|
});
|
|
339
|
+
_this.lastPromptHash = pHash;
|
|
264
340
|
|
|
265
341
|
return originalEnd.apply(this, endArgs);
|
|
266
342
|
};
|
|
267
343
|
|
|
344
|
+
// --- EMIT RESPONSE ---
|
|
268
345
|
clientRequest.on('response', (res) => {
|
|
269
346
|
const resChunks = [];
|
|
270
347
|
res.on('data', (chunk) => resChunks.push(chunk));
|
|
271
348
|
res.on('end', () => {
|
|
272
|
-
|
|
349
|
+
let resBody = Buffer.concat(resChunks).toString('utf8');
|
|
350
|
+
if (resBody && resBody.length > _this.maxOutputBytes) {
|
|
351
|
+
resBody = resBody.slice(0, _this.maxOutputBytes);
|
|
352
|
+
}
|
|
273
353
|
const latency = Date.now() - start;
|
|
274
|
-
const
|
|
354
|
+
const oHash = _this.hash(resBody);
|
|
275
355
|
|
|
276
356
|
_this.emit({
|
|
277
357
|
session_id: global.__dropout_session_id__,
|
|
278
|
-
turn_index: 0,
|
|
358
|
+
turn_index: clientRequest._dropout_turn || 0,
|
|
279
359
|
turn_role: 'assistant',
|
|
280
360
|
latency_ms: latency,
|
|
281
|
-
provider:
|
|
282
|
-
model:
|
|
361
|
+
provider: clientRequest._dropout_provider,
|
|
362
|
+
model: clientRequest._dropout_model,
|
|
283
363
|
content: _this.privacy === 'full' ? resBody : null,
|
|
284
|
-
|
|
364
|
+
content_hash: oHash,
|
|
365
|
+
metadata_flags: {
|
|
366
|
+
status: res.statusCode,
|
|
367
|
+
non_adaptive_response: oHash === _this.lastResponseHash ? 1 : 0,
|
|
368
|
+
}
|
|
285
369
|
});
|
|
370
|
+
|
|
371
|
+
_this.lastResponseHash = oHash;
|
|
372
|
+
_this.lastTurnConfirmed = true;
|
|
286
373
|
});
|
|
287
374
|
});
|
|
288
375
|
|