@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +143 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.2.01",
3
+ "version": "0.2.03",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
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. Internal telemetry sender (Neutral naming)
4
- async function safeSendTelemetry(payload) {
5
- try {
6
- await originalFetch('http://localhost:4000/capture', {
7
- method: 'POST',
8
- headers: { 'Content-Type': 'application/json' },
9
- body: JSON.stringify(payload)
10
- });
11
- } catch (e) {
12
- // Fail silently
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 prompt = undefined;
141
+ let promptText = undefined;
93
142
  let model = undefined;
94
143
 
95
144
  if (init && typeof init.body === 'string') {
96
- prompt = init.body;
145
+ promptText = init.body;
97
146
  try {
98
- const bodyJson = JSON.parse(prompt);
147
+ const bodyJson = JSON.parse(promptText);
99
148
  model = bodyJson.model;
100
149
  } catch (e) { }
101
150
  }
102
151
 
103
- let output = undefined;
152
+ let outputText = undefined;
104
153
  if (!isStream && config.captureOutput) {
105
154
  try {
106
155
  const cloned = response.clone();
107
- output = await cloned.text();
156
+ outputText = await cloned.text();
108
157
  // 8. Hard Capping (Infra Rule: Trust but verify)
109
- if (output && output.length > config.maxOutputBytes) {
110
- output = output.slice(0, config.maxOutputBytes);
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: options.prompt,
151
- output: typeof result === 'string'
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);