dank-ai 1.0.42 → 1.0.46
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 +89 -0
- package/docker/entrypoint.js +48 -32
- package/lib/docker/manager.js +28 -1
- package/package.json +1 -1
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
|
package/docker/entrypoint.js
CHANGED
|
@@ -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.
|
|
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
|
|
546
|
-
|
|
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,
|
|
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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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,
|
|
1254
|
+
async processDirectPrompt(prompt, metadata = {}, systemFields = {}) {
|
|
1237
1255
|
const startTime = Date.now();
|
|
1238
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1350
|
+
metadata,
|
|
1351
|
+
...systemFields, // protocol, clientIp, etc.
|
|
1336
1352
|
error: error.message,
|
|
1337
1353
|
processingTime,
|
|
1338
1354
|
promptModified: finalPrompt !== prompt,
|
package/lib/docker/manager.js
CHANGED
|
@@ -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)
|
|
1885
|
+
const escapedValue = this.escapeDockerfileEnvValue(String(value));
|
|
1859
1886
|
return `ENV ${key}="${escapedValue}"`;
|
|
1860
1887
|
})
|
|
1861
1888
|
.join('\n');
|