@dropout-ai/runtime 0.2.2 → 0.2.4
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 +139 -17
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const sessionId =
|
|
|
7
7
|
global.__dropout_session_id__ ||
|
|
8
8
|
(global.__dropout_session_id__ = crypto.randomUUID());
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Internal telemetry sender
|
|
11
11
|
async function safeSendTelemetry(payload) {
|
|
12
12
|
try {
|
|
13
13
|
await originalFetch('http://localhost:4000/capture', {
|
|
@@ -20,7 +20,61 @@ async function safeSendTelemetry(payload) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const NEGATIVE_FEEDBACK = [
|
|
24
|
+
"not helpful",
|
|
25
|
+
"dont just dump",
|
|
26
|
+
"you are worst",
|
|
27
|
+
"you are stupid",
|
|
28
|
+
"are you sure",
|
|
29
|
+
"i thought you are intelligent",
|
|
30
|
+
"let it be"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const ADAPTATION_REQUESTS = [
|
|
34
|
+
"help me understand",
|
|
35
|
+
"explain better",
|
|
36
|
+
"dont just dump facts",
|
|
37
|
+
"in simple way",
|
|
38
|
+
"explain it properly"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
let turnIndex = 0;
|
|
42
|
+
let lastPrompt = null;
|
|
43
|
+
|
|
44
|
+
function getSimilarity(text1, text2) {
|
|
45
|
+
if (!text1 || !text2) return 0;
|
|
46
|
+
const words1 = new Set(text1.toLowerCase().split(/\s+/));
|
|
47
|
+
const words2 = new Set(text2.toLowerCase().split(/\s+/));
|
|
48
|
+
const intersection = new Set([...words1].filter(x => words2.has(x)));
|
|
49
|
+
return intersection.size / Math.min(words1.size, words2.size);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getIntentHash(text) {
|
|
53
|
+
if (!text) return null;
|
|
54
|
+
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const CAPABILITY_LIMITATIONS = [
|
|
58
|
+
"i cannot access", "i don't have access", "i do not have access",
|
|
59
|
+
"cannot browse", "real-time information", "as an ai", "my knowledge cutoff",
|
|
60
|
+
"i'm sorry, but i don't", "i am sorry, but i don't"
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const CHALLENGE_PATTERNS = ["but ", "why ", "how ", "cant you", "can't you", "are you sure", "openai", "chatgpt"];
|
|
64
|
+
|
|
65
|
+
function detectSignals(text, output = "") {
|
|
66
|
+
const t = (text || "").toLowerCase();
|
|
67
|
+
const o = (output || "").toLowerCase();
|
|
68
|
+
return {
|
|
69
|
+
negativeFeedback: NEGATIVE_FEEDBACK.some(p => t.includes(p)) ? 1 : 0,
|
|
70
|
+
adaptationRequest: ADAPTATION_REQUESTS.some(p => t.includes(p)) ? 1 : 0,
|
|
71
|
+
userChallenge: CHALLENGE_PATTERNS.some(p => t.includes(p)) ? 1 : 0,
|
|
72
|
+
capabilityLimitation: CAPABILITY_LIMITATIONS.some(p => o.includes(p)) ? 1 : 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
23
76
|
(function () {
|
|
77
|
+
"use strict";
|
|
24
78
|
// 1. Guard global.fetch existence and idempotency
|
|
25
79
|
if (typeof global.fetch !== 'function' || global.fetch.__dropout_patched__) {
|
|
26
80
|
return;
|
|
@@ -31,7 +85,9 @@ async function safeSendTelemetry(payload) {
|
|
|
31
85
|
version: 1,
|
|
32
86
|
captureOutput: true,
|
|
33
87
|
maxOutputBytes: 20000,
|
|
34
|
-
enabledProviders: ['openai']
|
|
88
|
+
enabledProviders: ['openai'],
|
|
89
|
+
privacyMode: 'safe', // 'safe' | 'full'
|
|
90
|
+
captureContent: false
|
|
35
91
|
};
|
|
36
92
|
|
|
37
93
|
// 3. Remote Config Fetch (Non-blocking, fire-and-forget)
|
|
@@ -96,41 +152,57 @@ async function safeSendTelemetry(payload) {
|
|
|
96
152
|
const isStream = contentType.includes('text/event-stream');
|
|
97
153
|
|
|
98
154
|
// 7. Safe Body Capture (Best effort)
|
|
99
|
-
let
|
|
155
|
+
let promptText = undefined;
|
|
100
156
|
let model = undefined;
|
|
101
157
|
|
|
102
158
|
if (init && typeof init.body === 'string') {
|
|
103
|
-
|
|
159
|
+
promptText = init.body;
|
|
104
160
|
try {
|
|
105
|
-
const bodyJson = JSON.parse(
|
|
161
|
+
const bodyJson = JSON.parse(promptText);
|
|
106
162
|
model = bodyJson.model;
|
|
107
163
|
} catch (e) { }
|
|
108
164
|
}
|
|
109
165
|
|
|
110
|
-
let
|
|
166
|
+
let outputText = undefined;
|
|
111
167
|
if (!isStream && config.captureOutput) {
|
|
112
168
|
try {
|
|
113
169
|
const cloned = response.clone();
|
|
114
|
-
|
|
170
|
+
outputText = await cloned.text();
|
|
115
171
|
// 8. Hard Capping (Infra Rule: Trust but verify)
|
|
116
|
-
if (
|
|
117
|
-
|
|
172
|
+
if (outputText && outputText.length > config.maxOutputBytes) {
|
|
173
|
+
outputText = outputText.slice(0, config.maxOutputBytes);
|
|
118
174
|
}
|
|
119
175
|
} catch (e) { }
|
|
120
176
|
}
|
|
121
177
|
|
|
178
|
+
const currentSignals = detectSignals(promptText, outputText);
|
|
179
|
+
const similarity = getSimilarity(lastPrompt, promptText);
|
|
180
|
+
const intentHash = getIntentHash(promptText);
|
|
181
|
+
|
|
182
|
+
const isSafe = config.privacyMode === 'safe' && !config.captureContent;
|
|
183
|
+
|
|
122
184
|
const payload = {
|
|
123
185
|
provider: url.includes('openai.com') ? 'openai' : 'unknown',
|
|
124
186
|
confidence: matchesKnownProvider(url) ? 'high' : 'heuristic',
|
|
125
187
|
url,
|
|
126
188
|
model,
|
|
127
|
-
prompt,
|
|
128
|
-
output,
|
|
129
189
|
latency_ms: latency,
|
|
130
190
|
timestamp: Math.floor(Date.now() / 1000),
|
|
131
|
-
session_id: sessionId
|
|
191
|
+
session_id: sessionId,
|
|
192
|
+
turn_index: turnIndex++,
|
|
193
|
+
intent_hash: intentHash,
|
|
194
|
+
similarity_prev: similarity,
|
|
195
|
+
negative_feedback: currentSignals.negativeFeedback,
|
|
196
|
+
adaptation_request: currentSignals.adaptationRequest,
|
|
197
|
+
user_challenge: currentSignals.userChallenge,
|
|
198
|
+
capability_limitation: currentSignals.capabilityLimitation,
|
|
199
|
+
// Redact content if in safe mode
|
|
200
|
+
prompt: isSafe ? undefined : promptText,
|
|
201
|
+
output: isSafe ? undefined : outputText,
|
|
132
202
|
};
|
|
133
203
|
|
|
204
|
+
lastPrompt = promptText;
|
|
205
|
+
|
|
134
206
|
// 9. Fire-and-forget
|
|
135
207
|
setTimeout(() => safeSendTelemetry(payload), 0);
|
|
136
208
|
}
|
|
@@ -146,24 +218,72 @@ async function safeSendTelemetry(payload) {
|
|
|
146
218
|
async function capture(target, options = {}) {
|
|
147
219
|
const start = Date.now();
|
|
148
220
|
|
|
221
|
+
// Support dropout.capture({ prompt, response, privacy: "full" })
|
|
222
|
+
if (typeof target === 'object' && target !== null && !target.then) {
|
|
223
|
+
const { prompt, response, privacy } = target;
|
|
224
|
+
const isSafe = (privacy || 'safe') === 'safe';
|
|
225
|
+
|
|
226
|
+
const currentSignals = detectSignals(prompt, response);
|
|
227
|
+
const similarity = getSimilarity(lastPrompt, prompt);
|
|
228
|
+
const intentHash = getIntentHash(prompt);
|
|
229
|
+
|
|
230
|
+
const payload = {
|
|
231
|
+
provider: 'manual-opt-in',
|
|
232
|
+
prompt: isSafe ? undefined : prompt,
|
|
233
|
+
output: isSafe ? undefined : response,
|
|
234
|
+
latency_ms: 0,
|
|
235
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
236
|
+
session_id: sessionId,
|
|
237
|
+
turn_index: turnIndex++,
|
|
238
|
+
intent_hash: intentHash,
|
|
239
|
+
similarity_prev: similarity,
|
|
240
|
+
negative_feedback: currentSignals.negativeFeedback,
|
|
241
|
+
adaptation_request: currentSignals.adaptationRequest,
|
|
242
|
+
user_challenge: currentSignals.userChallenge,
|
|
243
|
+
capability_limitation: currentSignals.capabilityLimitation,
|
|
244
|
+
mode: 'manual'
|
|
245
|
+
};
|
|
246
|
+
lastPrompt = prompt;
|
|
247
|
+
setTimeout(() => safeSendTelemetry(payload), 0);
|
|
248
|
+
return response;
|
|
249
|
+
}
|
|
250
|
+
|
|
149
251
|
try {
|
|
150
252
|
const result =
|
|
151
253
|
typeof target === 'function'
|
|
152
254
|
? await target()
|
|
153
255
|
: await Promise.resolve(target);
|
|
154
256
|
|
|
257
|
+
const isSafe = options.privacy !== 'full';
|
|
258
|
+
|
|
259
|
+
const promptText = options.prompt;
|
|
260
|
+
const resultText = typeof result === 'string'
|
|
261
|
+
? result
|
|
262
|
+
: JSON.stringify(result).slice(0, 20000);
|
|
263
|
+
|
|
264
|
+
const currentSignals = detectSignals(promptText, resultText);
|
|
265
|
+
const similarity = getSimilarity(lastPrompt, promptText);
|
|
266
|
+
const intentHash = getIntentHash(promptText);
|
|
267
|
+
|
|
155
268
|
const payload = {
|
|
156
269
|
provider: options.provider || 'manual',
|
|
157
270
|
model: options.model,
|
|
158
|
-
prompt:
|
|
159
|
-
output:
|
|
160
|
-
? result
|
|
161
|
-
: JSON.stringify(result).slice(0, 20000),
|
|
271
|
+
prompt: isSafe ? undefined : promptText,
|
|
272
|
+
output: isSafe ? undefined : resultText,
|
|
162
273
|
latency_ms: Date.now() - start,
|
|
163
274
|
timestamp: Math.floor(Date.now() / 1000),
|
|
275
|
+
session_id: sessionId,
|
|
276
|
+
turn_index: turnIndex++,
|
|
277
|
+
intent_hash: intentHash,
|
|
278
|
+
similarity_prev: similarity,
|
|
279
|
+
negative_feedback: currentSignals.negativeFeedback,
|
|
280
|
+
adaptation_request: currentSignals.adaptationRequest,
|
|
281
|
+
user_challenge: currentSignals.userChallenge,
|
|
282
|
+
capability_limitation: currentSignals.capabilityLimitation,
|
|
164
283
|
mode: 'manual'
|
|
165
284
|
};
|
|
166
285
|
|
|
286
|
+
lastPrompt = promptText;
|
|
167
287
|
setTimeout(() => safeSendTelemetry(payload), 0);
|
|
168
288
|
return result;
|
|
169
289
|
} catch (error) {
|
|
@@ -172,7 +292,9 @@ async function capture(target, options = {}) {
|
|
|
172
292
|
error: error?.message || String(error),
|
|
173
293
|
latency_ms: Date.now() - start,
|
|
174
294
|
timestamp: Math.floor(Date.now() / 1000),
|
|
175
|
-
mode: 'manual'
|
|
295
|
+
mode: 'manual',
|
|
296
|
+
session_id: sessionId,
|
|
297
|
+
turn_index: turnIndex++
|
|
176
298
|
};
|
|
177
299
|
|
|
178
300
|
setTimeout(() => safeSendTelemetry(payload), 0);
|