dank-ai 1.0.42 → 1.0.45

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/README.md CHANGED
@@ -241,6 +241,95 @@ agent
241
241
 
242
242
  **Event Flow**: `request_output:start` → LLM Processing → `request_output` → `request_output:end` → Response Sent
243
243
 
244
+ #### Passing Custom Data to Handlers
245
+
246
+ You can pass any custom data in the request body to the `/prompt` endpoint, and it will be available in your handlers via `data.metadata`. This enables powerful use cases like user authentication, conversation tracking, RAG (Retrieval-Augmented Generation), and custom lookups.
247
+
248
+ **Client Request:**
249
+ ```javascript
250
+ // POST /prompt
251
+ {
252
+ "prompt": "What's the weather today?",
253
+ "userId": "user-12345",
254
+ "conversationId": "conv-abc-xyz",
255
+ "sessionId": "sess-789",
256
+ "userPreferences": {
257
+ "language": "en",
258
+ "timezone": "America/New_York"
259
+ }
260
+ }
261
+ ```
262
+
263
+ **Handler Access:**
264
+ ```javascript
265
+ agent
266
+ .addHandler('request_output:start', async (data) => {
267
+ // Access custom data via data.metadata
268
+ const userId = data.metadata.userId;
269
+ const conversationId = data.metadata.conversationId;
270
+
271
+ // Perform authentication
272
+ const user = await authenticateUser(userId);
273
+ if (!user) throw new Error('Unauthorized');
274
+
275
+ // Load conversation history for context
276
+ const history = await getConversationHistory(conversationId);
277
+
278
+ // Perform RAG lookup
279
+ const relevantDocs = await vectorSearch(data.prompt, userId);
280
+
281
+ // Enhance prompt with context
282
+ return {
283
+ prompt: `Context: ${JSON.stringify(history)}\n\nRelevant Docs: ${relevantDocs}\n\nUser Question: ${data.prompt}`
284
+ };
285
+ })
286
+
287
+ .addHandler('request_output', async (data) => {
288
+ // Log with user context
289
+ await logInteraction({
290
+ userId: data.metadata.userId,
291
+ conversationId: data.metadata.conversationId,
292
+ prompt: data.prompt,
293
+ response: data.response,
294
+ timestamp: data.timestamp
295
+ });
296
+
297
+ // Update user preferences based on interaction
298
+ if (data.metadata.userPreferences) {
299
+ await updateUserPreferences(data.metadata.userId, data.metadata.userPreferences);
300
+ }
301
+ });
302
+ ```
303
+
304
+ **Use Cases:**
305
+ - **User Authentication**: Pass `userId` or `apiKey` to authenticate and authorize requests
306
+ - **Conversation Tracking**: Pass `conversationId` to maintain context across multiple requests
307
+ - **RAG (Retrieval-Augmented Generation)**: Pass user context to fetch relevant documents from vector databases
308
+ - **Personalization**: Pass `userPreferences` to customize responses
309
+ - **Analytics**: Pass tracking IDs to correlate requests with user sessions
310
+ - **Multi-tenancy**: Pass `tenantId` or `organizationId` for isolated data access
311
+
312
+ **Available Data Structure:**
313
+ ```javascript
314
+ {
315
+ prompt: "User's prompt",
316
+ metadata: {
317
+ // All custom fields from request body
318
+ userId: "...",
319
+ conversationId: "...",
320
+ // ... any other fields you pass
321
+ },
322
+ // System fields (directly on data object)
323
+ protocol: "http",
324
+ clientIp: "127.0.0.1",
325
+ response: "LLM response",
326
+ usage: { total_tokens: 150 },
327
+ model: "gpt-3.5-turbo",
328
+ processingTime: 1234,
329
+ timestamp: "2024-01-01T00:00:00.000Z"
330
+ }
331
+ ```
332
+
244
333
  #### Tool Events (`tool:*`)
245
334
 
246
335
  ```javascript
@@ -494,10 +494,11 @@ class AgentRuntime {
494
494
  }
495
495
 
496
496
  /**
497
- * Emit an event to all matching handlers
497
+ * Emit an event to all matching handlers (fire-and-forget)
498
+ * Supports both sync and async handlers
498
499
  * Supports pattern matching for tool events
499
500
  */
500
- emitEvent(eventName, data = null) {
501
+ async emitEvent(eventName, data = null) {
501
502
  // Find all matching handlers (exact match and pattern match)
502
503
  const matchingHandlers = [];
503
504
 
@@ -507,19 +508,27 @@ class AgentRuntime {
507
508
  }
508
509
  }
509
510
 
510
- // Execute all matching handlers
511
- matchingHandlers.forEach((handler) => {
511
+ // Execute all matching handlers (in parallel for fire-and-forget)
512
+ const promises = matchingHandlers.map(async (handler) => {
512
513
  try {
514
+ let result;
513
515
  if (typeof handler === "function") {
514
- handler(data);
516
+ result = handler(data);
515
517
  } else if (handler.handler && typeof handler.handler === "function") {
516
- handler.handler(data);
518
+ result = handler.handler(data);
519
+ }
520
+ // Await if handler returns a promise
521
+ if (result && typeof result.then === "function") {
522
+ await result;
517
523
  }
518
524
  } catch (error) {
519
525
  logger.error(`Error in event handler for '${eventName}':`, error);
520
526
  }
521
527
  });
522
528
 
529
+ // Wait for all handlers to complete
530
+ await Promise.all(promises);
531
+
523
532
  if (matchingHandlers.length > 0) {
524
533
  logger.debug(
525
534
  `Emitted event '${eventName}' to ${matchingHandlers.length} handlers`
@@ -529,8 +538,10 @@ class AgentRuntime {
529
538
 
530
539
  /**
531
540
  * Emit an event and collect responses from handlers that return modified data
541
+ * Supports both sync and async handlers
542
+ * Handlers are executed sequentially to allow proper chaining of modifications
532
543
  */
533
- emitEventWithResponse(eventName, data) {
544
+ async emitEventWithResponse(eventName, data) {
534
545
  // Find all matching handlers (exact match and pattern match)
535
546
  const matchingHandlers = [];
536
547
 
@@ -542,8 +553,8 @@ class AgentRuntime {
542
553
 
543
554
  let modifiedData = { ...data };
544
555
 
545
- // Execute all matching handlers and collect responses
546
- matchingHandlers.forEach((handler) => {
556
+ // Execute handlers sequentially to allow chaining of modifications
557
+ for (const handler of matchingHandlers) {
547
558
  try {
548
559
  let result;
549
560
  if (typeof handler === "function") {
@@ -552,6 +563,11 @@ class AgentRuntime {
552
563
  result = handler.handler(modifiedData);
553
564
  }
554
565
 
566
+ // Await if handler returns a promise
567
+ if (result && typeof result.then === "function") {
568
+ result = await result;
569
+ }
570
+
555
571
  // If handler returns an object, merge it with the current data
556
572
  if (result && typeof result === "object" && !Array.isArray(result)) {
557
573
  modifiedData = { ...modifiedData, ...result };
@@ -559,7 +575,7 @@ class AgentRuntime {
559
575
  } catch (error) {
560
576
  logger.error(`Handler error for event '${eventName}':`, error);
561
577
  }
562
- });
578
+ }
563
579
 
564
580
  if (matchingHandlers.length > 0) {
565
581
  logger.debug(
@@ -1193,7 +1209,7 @@ class AgentRuntime {
1193
1209
  this.mainApp.post("/prompt", async (req, res) => {
1194
1210
  logger.info(`📥 Received POST /prompt request from ${req.ip || 'unknown'}`);
1195
1211
  try {
1196
- const { prompt, conversationId, metadata } = req.body;
1212
+ const { prompt, ...requestBodyFields } = req.body;
1197
1213
 
1198
1214
  if (!prompt) {
1199
1215
  return res.status(400).json({
@@ -1202,16 +1218,18 @@ class AgentRuntime {
1202
1218
  });
1203
1219
  }
1204
1220
 
1205
- const response = await this.processDirectPrompt(prompt, {
1206
- conversationId,
1207
- metadata,
1221
+ // Build metadata object with all user-provided fields from request body (except prompt)
1222
+ const metadata = {
1223
+ ...requestBodyFields,
1224
+ };
1225
+
1226
+ const response = await this.processDirectPrompt(prompt, metadata, {
1208
1227
  protocol: "http",
1209
1228
  clientIp: req.ip,
1210
1229
  });
1211
1230
 
1212
1231
  res.json({
1213
1232
  response: response.content,
1214
- conversationId: conversationId || response.conversationId,
1215
1233
  metadata: response.metadata,
1216
1234
  timestamp: new Date().toISOString(),
1217
1235
  });
@@ -1233,24 +1251,23 @@ class AgentRuntime {
1233
1251
  /**
1234
1252
  * Process a direct prompt and emit events
1235
1253
  */
1236
- async processDirectPrompt(prompt, context = {}) {
1254
+ async processDirectPrompt(prompt, metadata = {}, systemFields = {}) {
1237
1255
  const startTime = Date.now();
1238
- const conversationId = context.conversationId || require("uuid").v4();
1256
+ let finalPrompt = prompt; // Declare outside try block so it's available in catch
1239
1257
 
1240
1258
  try {
1241
1259
  // Emit request start event and allow handlers to modify the prompt
1242
1260
  const startEventData = {
1243
1261
  prompt,
1244
- conversationId,
1245
- context,
1262
+ metadata,
1263
+ ...systemFields, // protocol, clientIp, etc.
1246
1264
  timestamp: new Date().toISOString(),
1247
1265
  };
1248
1266
 
1249
- const modifiedData = this.emitEventWithResponse("request_output:start", startEventData);
1267
+ const modifiedData = await this.emitEventWithResponse("request_output:start", startEventData);
1250
1268
 
1251
1269
  // Use modified prompt if handlers returned one, otherwise use original
1252
- const finalPrompt = modifiedData?.prompt || prompt;
1253
-
1270
+ finalPrompt = modifiedData?.prompt || prompt;
1254
1271
  // Process with LLM
1255
1272
  let response;
1256
1273
  if (this.llmProvider === "openai") {
@@ -1281,12 +1298,12 @@ class AgentRuntime {
1281
1298
  const processingTime = Date.now() - startTime;
1282
1299
 
1283
1300
  // Emit request output event
1284
- this.emitEvent("request_output", {
1301
+ await this.emitEvent("request_output", {
1285
1302
  prompt,
1286
1303
  finalPrompt, // Include the final prompt that was sent to LLM
1287
1304
  response: response.content,
1288
- conversationId,
1289
- context,
1305
+ metadata,
1306
+ ...systemFields, // protocol, clientIp, etc.
1290
1307
  usage: response.usage,
1291
1308
  model: response.model,
1292
1309
  processingTime,
@@ -1296,11 +1313,11 @@ class AgentRuntime {
1296
1313
 
1297
1314
  // Emit end event and allow handlers to modify the response before returning
1298
1315
  const endEventData = {
1299
- conversationId,
1300
1316
  prompt,
1301
1317
  finalPrompt,
1302
1318
  response: response.content,
1303
- context,
1319
+ metadata,
1320
+ ...systemFields, // protocol, clientIp, etc.
1304
1321
  usage: response.usage,
1305
1322
  model: response.model,
1306
1323
  processingTime,
@@ -1309,14 +1326,13 @@ class AgentRuntime {
1309
1326
  timestamp: new Date().toISOString(),
1310
1327
  };
1311
1328
 
1312
- const modifiedEndData = this.emitEventWithResponse("request_output:end", endEventData);
1329
+ const modifiedEndData = await this.emitEventWithResponse("request_output:end", endEventData);
1313
1330
 
1314
1331
  // Use the final response (potentially modified by handlers)
1315
1332
  const finalResponse = modifiedEndData.response || response.content;
1316
1333
 
1317
1334
  return {
1318
1335
  content: finalResponse,
1319
- conversationId,
1320
1336
  metadata: {
1321
1337
  usage: response.usage,
1322
1338
  model: response.model,
@@ -1328,11 +1344,11 @@ class AgentRuntime {
1328
1344
  const processingTime = Date.now() - startTime;
1329
1345
 
1330
1346
  // Emit error event
1331
- this.emitEvent("request_output:error", {
1347
+ await this.emitEvent("request_output:error", {
1332
1348
  prompt,
1333
1349
  finalPrompt,
1334
- conversationId,
1335
- context,
1350
+ metadata,
1351
+ ...systemFields, // protocol, clientIp, etc.
1336
1352
  error: error.message,
1337
1353
  processingTime,
1338
1354
  promptModified: finalPrompt !== prompt,
@@ -1051,6 +1051,33 @@ class DockerManager {
1051
1051
  return new Promise((resolve) => setTimeout(resolve, ms));
1052
1052
  }
1053
1053
 
1054
+ /**
1055
+ * Escape special characters in environment variable values for Dockerfile ENV statements
1056
+ * Handles newlines, quotes, backslashes, and Docker variable syntax
1057
+ *
1058
+ * @param {string} value - The environment variable value to escape
1059
+ * @returns {string} - Escaped value safe for Dockerfile ENV statements
1060
+ */
1061
+ escapeDockerfileEnvValue(value) {
1062
+ if (value === null || value === undefined) {
1063
+ return '';
1064
+ }
1065
+
1066
+ return String(value)
1067
+ // Escape backslashes first (must be first to avoid double-escaping)
1068
+ .replace(/\\/g, '\\\\')
1069
+ // Escape dollar signs for Dockerfile variable syntax ($VAR becomes $$VAR)
1070
+ .replace(/\$/g, '$$$$')
1071
+ // Escape double quotes
1072
+ .replace(/"/g, '\\"')
1073
+ // Escape newlines as \n (will be interpreted as newline when container reads env var)
1074
+ .replace(/\n/g, '\\n')
1075
+ // Escape carriage returns as \r
1076
+ .replace(/\r/g, '\\r')
1077
+ // Escape tabs as \t
1078
+ .replace(/\t/g, '\\t');
1079
+ }
1080
+
1054
1081
  /**
1055
1082
  * Pull the base Docker image
1056
1083
  */
@@ -1855,7 +1882,7 @@ class DockerManager {
1855
1882
  envStatements = Object.entries(env)
1856
1883
  .map(([key, value]) => {
1857
1884
  // Escape special characters in values for Dockerfile ENV
1858
- const escapedValue = String(value).replace(/\$/g, '$$$$').replace(/"/g, '\\"');
1885
+ const escapedValue = this.escapeDockerfileEnvValue(String(value));
1859
1886
  return `ENV ${key}="${escapedValue}"`;
1860
1887
  })
1861
1888
  .join('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dank-ai",
3
- "version": "1.0.42",
3
+ "version": "1.0.45",
4
4
  "description": "Dank Agent Service - Docker-based AI agent orchestration platform",
5
5
  "main": "lib/index.js",
6
6
  "exports": {