@dropout-ai/runtime 0.2.2 → 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 +135 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.2.02",
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
@@ -7,17 +7,57 @@ const sessionId =
7
7
  global.__dropout_session_id__ ||
8
8
  (global.__dropout_session_id__ = crypto.randomUUID());
9
9
 
10
- // 4. Internal telemetry sender (Neutral naming)
11
- async function safeSendTelemetry(payload) {
12
- try {
13
- await originalFetch('http://localhost:4000/capture', {
14
- method: 'POST',
15
- headers: { 'Content-Type': 'application/json' },
16
- body: JSON.stringify(payload)
17
- });
18
- } catch (e) {
19
- // Fail silently
20
- }
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
+ };
21
61
  }
22
62
 
23
63
  (function () {
@@ -31,7 +71,9 @@ async function safeSendTelemetry(payload) {
31
71
  version: 1,
32
72
  captureOutput: true,
33
73
  maxOutputBytes: 20000,
34
- enabledProviders: ['openai']
74
+ enabledProviders: ['openai'],
75
+ privacyMode: 'safe', // 'safe' | 'full'
76
+ captureContent: false
35
77
  };
36
78
 
37
79
  // 3. Remote Config Fetch (Non-blocking, fire-and-forget)
@@ -96,41 +138,57 @@ async function safeSendTelemetry(payload) {
96
138
  const isStream = contentType.includes('text/event-stream');
97
139
 
98
140
  // 7. Safe Body Capture (Best effort)
99
- let prompt = undefined;
141
+ let promptText = undefined;
100
142
  let model = undefined;
101
143
 
102
144
  if (init && typeof init.body === 'string') {
103
- prompt = init.body;
145
+ promptText = init.body;
104
146
  try {
105
- const bodyJson = JSON.parse(prompt);
147
+ const bodyJson = JSON.parse(promptText);
106
148
  model = bodyJson.model;
107
149
  } catch (e) { }
108
150
  }
109
151
 
110
- let output = undefined;
152
+ let outputText = undefined;
111
153
  if (!isStream && config.captureOutput) {
112
154
  try {
113
155
  const cloned = response.clone();
114
- output = await cloned.text();
156
+ outputText = await cloned.text();
115
157
  // 8. Hard Capping (Infra Rule: Trust but verify)
116
- if (output && output.length > config.maxOutputBytes) {
117
- output = output.slice(0, config.maxOutputBytes);
158
+ if (outputText && outputText.length > config.maxOutputBytes) {
159
+ outputText = outputText.slice(0, config.maxOutputBytes);
118
160
  }
119
161
  } catch (e) { }
120
162
  }
121
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
+
122
170
  const payload = {
123
171
  provider: url.includes('openai.com') ? 'openai' : 'unknown',
124
172
  confidence: matchesKnownProvider(url) ? 'high' : 'heuristic',
125
173
  url,
126
174
  model,
127
- prompt,
128
- output,
129
175
  latency_ms: latency,
130
176
  timestamp: Math.floor(Date.now() / 1000),
131
- session_id: sessionId
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,
132
188
  };
133
189
 
190
+ lastPrompt = promptText;
191
+
134
192
  // 9. Fire-and-forget
135
193
  setTimeout(() => safeSendTelemetry(payload), 0);
136
194
  }
@@ -146,24 +204,72 @@ async function safeSendTelemetry(payload) {
146
204
  async function capture(target, options = {}) {
147
205
  const start = Date.now();
148
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
+
149
237
  try {
150
238
  const result =
151
239
  typeof target === 'function'
152
240
  ? await target()
153
241
  : await Promise.resolve(target);
154
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
+
155
254
  const payload = {
156
255
  provider: options.provider || 'manual',
157
256
  model: options.model,
158
- prompt: options.prompt,
159
- output: typeof result === 'string'
160
- ? result
161
- : JSON.stringify(result).slice(0, 20000),
257
+ prompt: isSafe ? undefined : promptText,
258
+ output: isSafe ? undefined : resultText,
162
259
  latency_ms: Date.now() - start,
163
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,
164
269
  mode: 'manual'
165
270
  };
166
271
 
272
+ lastPrompt = promptText;
167
273
  setTimeout(() => safeSendTelemetry(payload), 0);
168
274
  return result;
169
275
  } catch (error) {
@@ -172,7 +278,9 @@ async function capture(target, options = {}) {
172
278
  error: error?.message || String(error),
173
279
  latency_ms: Date.now() - start,
174
280
  timestamp: Math.floor(Date.now() / 1000),
175
- mode: 'manual'
281
+ mode: 'manual',
282
+ session_id: sessionId,
283
+ turn_index: turnIndex++
176
284
  };
177
285
 
178
286
  setTimeout(() => safeSendTelemetry(payload), 0);