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