dank-ai 1.0.0
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 +1331 -0
- package/bin/dank +118 -0
- package/docker/Dockerfile +57 -0
- package/docker/entrypoint.js +1227 -0
- package/docker/package.json +19 -0
- package/lib/agent.js +644 -0
- package/lib/cli/build.js +43 -0
- package/lib/cli/clean.js +30 -0
- package/lib/cli/init.js +38 -0
- package/lib/cli/logs.js +122 -0
- package/lib/cli/run.js +176 -0
- package/lib/cli/status.js +125 -0
- package/lib/cli/stop.js +87 -0
- package/lib/config.js +180 -0
- package/lib/constants.js +58 -0
- package/lib/docker/manager.js +968 -0
- package/lib/index.js +26 -0
- package/lib/project.js +280 -0
- package/lib/tools/builtin.js +445 -0
- package/lib/tools/index.js +335 -0
- package/package.json +52 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dank Agent Container Entrypoint
|
|
5
|
+
*
|
|
6
|
+
* This script runs inside each agent container and handles:
|
|
7
|
+
* - Loading agent code from the drop-off directory
|
|
8
|
+
* - Setting up the LLM client
|
|
9
|
+
* - Managing agent lifecycle
|
|
10
|
+
* - Health checks and monitoring
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
const express = require("express");
|
|
16
|
+
const winston = require("winston");
|
|
17
|
+
const { v4: uuidv4 } = require("uuid");
|
|
18
|
+
|
|
19
|
+
// Load environment variables
|
|
20
|
+
require("dotenv").config();
|
|
21
|
+
|
|
22
|
+
// Setup logging
|
|
23
|
+
const logger = winston.createLogger({
|
|
24
|
+
level: process.env.LOG_LEVEL || "info",
|
|
25
|
+
format: winston.format.combine(
|
|
26
|
+
winston.format.timestamp(),
|
|
27
|
+
winston.format.errors({ stack: true }),
|
|
28
|
+
winston.format.json()
|
|
29
|
+
),
|
|
30
|
+
transports: [
|
|
31
|
+
new winston.transports.Console({
|
|
32
|
+
format: winston.format.combine(
|
|
33
|
+
winston.format.colorize(),
|
|
34
|
+
winston.format.simple()
|
|
35
|
+
),
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
class AgentRuntime {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.agentName = process.env.AGENT_NAME || "unknown";
|
|
43
|
+
this.agentId = process.env.AGENT_ID || uuidv4();
|
|
44
|
+
this.llmProvider = process.env.LLM_PROVIDER || "openai";
|
|
45
|
+
this.llmModel = process.env.LLM_MODEL || "gpt-3.5-turbo";
|
|
46
|
+
this.agentPrompt =
|
|
47
|
+
process.env.AGENT_PROMPT || "You are a helpful AI assistant.";
|
|
48
|
+
|
|
49
|
+
this.llmClient = null;
|
|
50
|
+
this.agentCode = null;
|
|
51
|
+
this.handlers = new Map();
|
|
52
|
+
this.isRunning = false;
|
|
53
|
+
this.startTime = new Date();
|
|
54
|
+
|
|
55
|
+
// HTTP server configuration
|
|
56
|
+
this.httpEnabled = process.env.HTTP_ENABLED === "true";
|
|
57
|
+
this.httpPort = parseInt(process.env.HTTP_PORT) || 3000;
|
|
58
|
+
this.httpHost = process.env.HTTP_HOST || "0.0.0.0";
|
|
59
|
+
|
|
60
|
+
// Setup express servers
|
|
61
|
+
this.healthApp = express(); // Health check server (always running)
|
|
62
|
+
this.httpApp = null; // Main HTTP server (optional)
|
|
63
|
+
|
|
64
|
+
this.setupHealthEndpoints();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Initialize the agent runtime
|
|
69
|
+
*/
|
|
70
|
+
async initialize() {
|
|
71
|
+
try {
|
|
72
|
+
logger.info(`Initializing agent: ${this.agentName} (${this.agentId})`);
|
|
73
|
+
|
|
74
|
+
// Load agent code
|
|
75
|
+
await this.loadAgentCode();
|
|
76
|
+
|
|
77
|
+
// Initialize LLM client
|
|
78
|
+
await this.initializeLLM();
|
|
79
|
+
|
|
80
|
+
// Setup agent handlers
|
|
81
|
+
await this.setupHandlers();
|
|
82
|
+
|
|
83
|
+
// Start health check server
|
|
84
|
+
this.startHealthServer();
|
|
85
|
+
|
|
86
|
+
// Start HTTP server if enabled
|
|
87
|
+
if (this.httpEnabled) {
|
|
88
|
+
await this.setupHttpServer();
|
|
89
|
+
this.startHttpServer();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Setup direct prompting server
|
|
93
|
+
await this.setupDirectPromptingServer();
|
|
94
|
+
|
|
95
|
+
// Mark as running
|
|
96
|
+
this.isRunning = true;
|
|
97
|
+
|
|
98
|
+
logger.info(`Agent ${this.agentName} initialized successfully`);
|
|
99
|
+
|
|
100
|
+
// Execute agent main function if it exists
|
|
101
|
+
if (this.agentCode && typeof this.agentCode.main === "function") {
|
|
102
|
+
// Create agent context with tools and capabilities
|
|
103
|
+
const agentContext = {
|
|
104
|
+
llmClient: this.llmClient,
|
|
105
|
+
handlers: this.handlers,
|
|
106
|
+
tools: this.createToolsProxy(),
|
|
107
|
+
config: {
|
|
108
|
+
name: this.agentName,
|
|
109
|
+
id: this.agentId,
|
|
110
|
+
prompt: this.agentPrompt,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Execute main function without awaiting to prevent blocking
|
|
115
|
+
// This allows the agent to run asynchronously while keeping the container alive
|
|
116
|
+
this.executeAgentMain(agentContext);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Keep the container alive - this is essential for agent runtime
|
|
120
|
+
this.keepAlive();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error("Failed to initialize agent:", error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Execute agent main function with proper error handling
|
|
129
|
+
*/
|
|
130
|
+
async executeAgentMain(agentContext) {
|
|
131
|
+
try {
|
|
132
|
+
logger.info("Executing agent main function...");
|
|
133
|
+
|
|
134
|
+
// Call the main function and handle different return patterns
|
|
135
|
+
const result = this.agentCode.main(agentContext);
|
|
136
|
+
|
|
137
|
+
// If it returns a promise, handle it properly
|
|
138
|
+
if (result && typeof result.then === "function") {
|
|
139
|
+
result.catch((error) => {
|
|
140
|
+
logger.error("Agent main function error:", error);
|
|
141
|
+
// Don't exit the container, just log the error
|
|
142
|
+
this.handlers.get("error")?.forEach((handler) => {
|
|
143
|
+
try {
|
|
144
|
+
handler(error);
|
|
145
|
+
} catch (handlerError) {
|
|
146
|
+
logger.error("Error handler failed:", handlerError);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
logger.info("Agent main function started successfully");
|
|
153
|
+
} catch (error) {
|
|
154
|
+
logger.error("Failed to execute agent main function:", error);
|
|
155
|
+
// Don't exit the container, just log the error and continue
|
|
156
|
+
this.handlers.get("error")?.forEach((handler) => {
|
|
157
|
+
try {
|
|
158
|
+
handler(error);
|
|
159
|
+
} catch (handlerError) {
|
|
160
|
+
logger.error("Error handler failed:", handlerError);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Keep the container alive with a heartbeat mechanism
|
|
168
|
+
*/
|
|
169
|
+
keepAlive() {
|
|
170
|
+
logger.info("Starting keep-alive mechanism...");
|
|
171
|
+
|
|
172
|
+
// Set up a heartbeat interval to keep the container running
|
|
173
|
+
this.heartbeatInterval = setInterval(() => {
|
|
174
|
+
if (this.isRunning) {
|
|
175
|
+
logger.debug(
|
|
176
|
+
`Agent ${this.agentName} heartbeat - uptime: ${Math.floor(
|
|
177
|
+
process.uptime()
|
|
178
|
+
)}s`
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Trigger heartbeat handlers
|
|
182
|
+
const heartbeatHandlers = this.handlers.get("heartbeat") || [];
|
|
183
|
+
heartbeatHandlers.forEach((handler) => {
|
|
184
|
+
try {
|
|
185
|
+
handler();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.error("Heartbeat handler error:", error);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}, 30000); // Heartbeat every 30 seconds
|
|
192
|
+
|
|
193
|
+
// Also set up a simple keep-alive mechanism
|
|
194
|
+
// This ensures the event loop stays active
|
|
195
|
+
this.keepAliveTimeout = setTimeout(() => {
|
|
196
|
+
// This timeout will never fire, but keeps the event loop active
|
|
197
|
+
logger.debug("Keep-alive timeout triggered (this should not happen)");
|
|
198
|
+
}, 2147483647); // Maximum timeout value
|
|
199
|
+
|
|
200
|
+
logger.info("Keep-alive mechanism started - container will stay running");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load agent code from drop-off directory
|
|
205
|
+
*/
|
|
206
|
+
async loadAgentCode() {
|
|
207
|
+
const codeDir = "/app/agent-code";
|
|
208
|
+
const mainFile = path.join(codeDir, "index.js");
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(mainFile)) {
|
|
211
|
+
logger.info("Loading agent code from index.js");
|
|
212
|
+
this.agentCode = require(mainFile);
|
|
213
|
+
} else {
|
|
214
|
+
logger.warn("No agent code found, running in basic mode");
|
|
215
|
+
this.agentCode = {
|
|
216
|
+
main: async (agentContext) => {
|
|
217
|
+
logger.info("Agent running in basic mode - no custom code loaded");
|
|
218
|
+
logger.info(
|
|
219
|
+
"Basic mode agent is ready and will respond to HTTP requests if enabled"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// In basic mode, the agent just stays alive and responds to HTTP requests
|
|
223
|
+
// The keep-alive mechanism will handle keeping the container running
|
|
224
|
+
return Promise.resolve();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Initialize LLM client based on provider
|
|
232
|
+
*/
|
|
233
|
+
async initializeLLM() {
|
|
234
|
+
const apiKey = process.env.LLM_API_KEY;
|
|
235
|
+
const baseURL = process.env.LLM_BASE_URL;
|
|
236
|
+
|
|
237
|
+
switch (this.llmProvider) {
|
|
238
|
+
case "openai":
|
|
239
|
+
const { OpenAI } = require("openai");
|
|
240
|
+
this.llmClient = new OpenAI({
|
|
241
|
+
apiKey,
|
|
242
|
+
baseURL,
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
|
|
246
|
+
case "anthropic":
|
|
247
|
+
const { Anthropic } = require("@anthropic-ai/sdk");
|
|
248
|
+
this.llmClient = new Anthropic({
|
|
249
|
+
apiKey,
|
|
250
|
+
});
|
|
251
|
+
break;
|
|
252
|
+
|
|
253
|
+
case "cohere":
|
|
254
|
+
const { CohereClient } = require("cohere-ai");
|
|
255
|
+
this.llmClient = new CohereClient({
|
|
256
|
+
token: apiKey,
|
|
257
|
+
});
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case "ollama":
|
|
261
|
+
// Custom implementation for Ollama
|
|
262
|
+
const axios = require("axios");
|
|
263
|
+
this.llmClient = {
|
|
264
|
+
baseURL: baseURL || "http://localhost:11434",
|
|
265
|
+
async chat(messages) {
|
|
266
|
+
const response = await axios.post(`${this.baseURL}/api/chat`, {
|
|
267
|
+
model: process.env.LLM_MODEL,
|
|
268
|
+
messages,
|
|
269
|
+
stream: false,
|
|
270
|
+
});
|
|
271
|
+
return response.data;
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
default:
|
|
277
|
+
throw new Error(`Unsupported LLM provider: ${this.llmProvider}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
logger.info(
|
|
281
|
+
`LLM client initialized: ${this.llmProvider} (${this.llmModel})`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Setup event handlers
|
|
287
|
+
*/
|
|
288
|
+
async setupHandlers() {
|
|
289
|
+
// Default handlers
|
|
290
|
+
this.handlers.set("output", [(data) => logger.info("Agent output:", data)]);
|
|
291
|
+
|
|
292
|
+
this.handlers.set("error", [
|
|
293
|
+
(error) => logger.error("Agent error:", error),
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
this.handlers.set("heartbeat", [
|
|
297
|
+
() => logger.debug(`Agent ${this.agentName} heartbeat`),
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
// Load custom handlers from agent code
|
|
301
|
+
if (this.agentCode && this.agentCode.handlers) {
|
|
302
|
+
Object.entries(this.agentCode.handlers).forEach(
|
|
303
|
+
([event, handlerList]) => {
|
|
304
|
+
if (!this.handlers.has(event)) {
|
|
305
|
+
this.handlers.set(event, []);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const handlers = Array.isArray(handlerList)
|
|
309
|
+
? handlerList
|
|
310
|
+
: [handlerList];
|
|
311
|
+
this.handlers.get(event).push(...handlers);
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
logger.info(
|
|
317
|
+
`Handlers setup complete. Events: ${Array.from(this.handlers.keys()).join(
|
|
318
|
+
", "
|
|
319
|
+
)}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Emit an event to all matching handlers
|
|
325
|
+
* Supports pattern matching for tool events
|
|
326
|
+
*/
|
|
327
|
+
emitEvent(eventName, data = null) {
|
|
328
|
+
// Find all matching handlers (exact match and pattern match)
|
|
329
|
+
const matchingHandlers = [];
|
|
330
|
+
|
|
331
|
+
for (const [handlerPattern, handlers] of this.handlers) {
|
|
332
|
+
if (this.matchesEventPattern(eventName, handlerPattern)) {
|
|
333
|
+
matchingHandlers.push(...handlers);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Execute all matching handlers
|
|
338
|
+
matchingHandlers.forEach((handler) => {
|
|
339
|
+
try {
|
|
340
|
+
if (typeof handler === "function") {
|
|
341
|
+
handler(data);
|
|
342
|
+
} else if (handler.handler && typeof handler.handler === "function") {
|
|
343
|
+
handler.handler(data);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
logger.error(`Error in event handler for '${eventName}':`, error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (matchingHandlers.length > 0) {
|
|
351
|
+
logger.debug(
|
|
352
|
+
`Emitted event '${eventName}' to ${matchingHandlers.length} handlers`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Emit an event and collect responses from handlers that return modified data
|
|
359
|
+
*/
|
|
360
|
+
emitEventWithResponse(eventName, data) {
|
|
361
|
+
// Find all matching handlers (exact match and pattern match)
|
|
362
|
+
const matchingHandlers = [];
|
|
363
|
+
|
|
364
|
+
for (const [handlerPattern, handlers] of this.handlers) {
|
|
365
|
+
if (this.matchesEventPattern(eventName, handlerPattern)) {
|
|
366
|
+
matchingHandlers.push(...handlers);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let modifiedData = { ...data };
|
|
371
|
+
|
|
372
|
+
// Execute all matching handlers and collect responses
|
|
373
|
+
matchingHandlers.forEach((handler) => {
|
|
374
|
+
try {
|
|
375
|
+
let result;
|
|
376
|
+
if (typeof handler === "function") {
|
|
377
|
+
result = handler(modifiedData);
|
|
378
|
+
} else if (handler.handler && typeof handler.handler === "function") {
|
|
379
|
+
result = handler.handler(modifiedData);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// If handler returns an object, merge it with the current data
|
|
383
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
384
|
+
modifiedData = { ...modifiedData, ...result };
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
logger.error(`Handler error for event '${eventName}':`, error);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
if (matchingHandlers.length > 0) {
|
|
392
|
+
logger.debug(
|
|
393
|
+
`Emitted event '${eventName}' to ${matchingHandlers.length} handlers with response capability`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return modifiedData;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Check if an event name matches a handler pattern
|
|
402
|
+
* Supports wildcards and specific patterns
|
|
403
|
+
*/
|
|
404
|
+
matchesEventPattern(eventName, pattern) {
|
|
405
|
+
// Exact match
|
|
406
|
+
if (eventName === pattern) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Wildcard patterns
|
|
411
|
+
if (pattern.includes("*")) {
|
|
412
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/:/g, ":");
|
|
413
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
414
|
+
return regex.test(eventName);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Setup health check endpoints
|
|
422
|
+
*/
|
|
423
|
+
setupHealthEndpoints() {
|
|
424
|
+
this.healthApp.get("/health", (req, res) => {
|
|
425
|
+
res.json({
|
|
426
|
+
status: this.isRunning ? "healthy" : "starting",
|
|
427
|
+
agent: {
|
|
428
|
+
name: this.agentName,
|
|
429
|
+
id: this.agentId,
|
|
430
|
+
provider: this.llmProvider,
|
|
431
|
+
model: this.llmModel,
|
|
432
|
+
},
|
|
433
|
+
http: {
|
|
434
|
+
enabled: this.httpEnabled,
|
|
435
|
+
port: this.httpEnabled ? this.httpPort : null,
|
|
436
|
+
},
|
|
437
|
+
uptime: Date.now() - this.startTime.getTime(),
|
|
438
|
+
timestamp: new Date().toISOString(),
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
this.healthApp.get("/status", (req, res) => {
|
|
443
|
+
res.json({
|
|
444
|
+
agent: {
|
|
445
|
+
name: this.agentName,
|
|
446
|
+
id: this.agentId,
|
|
447
|
+
status: this.isRunning ? "running" : "starting",
|
|
448
|
+
startTime: this.startTime.toISOString(),
|
|
449
|
+
uptime: Date.now() - this.startTime.getTime(),
|
|
450
|
+
},
|
|
451
|
+
llm: {
|
|
452
|
+
provider: this.llmProvider,
|
|
453
|
+
model: this.llmModel,
|
|
454
|
+
hasClient: !!this.llmClient,
|
|
455
|
+
},
|
|
456
|
+
http: {
|
|
457
|
+
enabled: this.httpEnabled,
|
|
458
|
+
port: this.httpEnabled ? this.httpPort : null,
|
|
459
|
+
routes: this.httpEnabled && this.httpApp ? this.getRoutesList() : [],
|
|
460
|
+
},
|
|
461
|
+
handlers: Array.from(this.handlers.keys()),
|
|
462
|
+
environment: {
|
|
463
|
+
nodeVersion: process.version,
|
|
464
|
+
platform: process.platform,
|
|
465
|
+
memory: process.memoryUsage(),
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Setup HTTP server with Express.js
|
|
473
|
+
*/
|
|
474
|
+
async setupHttpServer() {
|
|
475
|
+
if (!this.httpEnabled) return;
|
|
476
|
+
|
|
477
|
+
logger.info(`Setting up HTTP server on port ${this.httpPort}`);
|
|
478
|
+
|
|
479
|
+
this.httpApp = express();
|
|
480
|
+
|
|
481
|
+
// Basic middleware
|
|
482
|
+
this.httpApp.use(express.json({ limit: "10mb" }));
|
|
483
|
+
this.httpApp.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
484
|
+
|
|
485
|
+
// CORS if enabled
|
|
486
|
+
if (process.env.HTTP_CORS !== "false") {
|
|
487
|
+
this.httpApp.use((req, res, next) => {
|
|
488
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
489
|
+
res.header(
|
|
490
|
+
"Access-Control-Allow-Methods",
|
|
491
|
+
"GET, POST, PUT, DELETE, PATCH, OPTIONS"
|
|
492
|
+
);
|
|
493
|
+
res.header(
|
|
494
|
+
"Access-Control-Allow-Headers",
|
|
495
|
+
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (req.method === "OPTIONS") {
|
|
499
|
+
res.sendStatus(200);
|
|
500
|
+
} else {
|
|
501
|
+
next();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Rate limiting if configured
|
|
507
|
+
if (process.env.HTTP_RATE_LIMIT === "true") {
|
|
508
|
+
const rateLimit = require("express-rate-limit");
|
|
509
|
+
const limiter = rateLimit({
|
|
510
|
+
windowMs:
|
|
511
|
+
parseInt(process.env.HTTP_RATE_LIMIT_WINDOW) || 15 * 60 * 1000,
|
|
512
|
+
max: parseInt(process.env.HTTP_RATE_LIMIT_MAX) || 100,
|
|
513
|
+
message: process.env.HTTP_RATE_LIMIT_MESSAGE || "Too many requests",
|
|
514
|
+
});
|
|
515
|
+
this.httpApp.use(limiter);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Setup routes from agent configuration
|
|
519
|
+
await this.setupAgentRoutes();
|
|
520
|
+
|
|
521
|
+
// Default routes
|
|
522
|
+
this.httpApp.get("/", (req, res) => {
|
|
523
|
+
res.json({
|
|
524
|
+
message: `🤖 ${this.agentName} HTTP Server`,
|
|
525
|
+
agent: this.agentName,
|
|
526
|
+
version: "1.0.0",
|
|
527
|
+
endpoints: this.getRoutesList(),
|
|
528
|
+
timestamp: new Date().toISOString(),
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// 404 handler
|
|
533
|
+
this.httpApp.use((req, res) => {
|
|
534
|
+
res.status(404).json({
|
|
535
|
+
error: "Not Found",
|
|
536
|
+
message: `Cannot ${req.method} ${req.path}`,
|
|
537
|
+
availableRoutes: this.getRoutesList(),
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Error handler
|
|
542
|
+
this.httpApp.use((err, req, res, next) => {
|
|
543
|
+
logger.error("HTTP server error:", err);
|
|
544
|
+
res.status(500).json({
|
|
545
|
+
error: "Internal Server Error",
|
|
546
|
+
message:
|
|
547
|
+
process.env.NODE_ENV === "development"
|
|
548
|
+
? err.message
|
|
549
|
+
: "Something went wrong",
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Setup routes defined in agent configuration
|
|
556
|
+
*/
|
|
557
|
+
async setupAgentRoutes() {
|
|
558
|
+
if (!this.agentCode || !this.agentCode.routes) return;
|
|
559
|
+
|
|
560
|
+
// Setup routes from agent code
|
|
561
|
+
Object.entries(this.agentCode.routes).forEach(([path, handlers]) => {
|
|
562
|
+
if (typeof handlers === "object") {
|
|
563
|
+
Object.entries(handlers).forEach(([method, handler]) => {
|
|
564
|
+
if (typeof handler === "function") {
|
|
565
|
+
const lowerMethod = method.toLowerCase();
|
|
566
|
+
if (this.httpApp[lowerMethod]) {
|
|
567
|
+
// Wrap handler to emit tool events
|
|
568
|
+
const wrappedHandler = this.wrapHttpHandlerWithEvents(
|
|
569
|
+
method,
|
|
570
|
+
path,
|
|
571
|
+
handler
|
|
572
|
+
);
|
|
573
|
+
this.httpApp[lowerMethod](path, wrappedHandler);
|
|
574
|
+
logger.info(`Registered route: ${method.toUpperCase()} ${path}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Setup middleware from agent code
|
|
582
|
+
if (this.agentCode.middleware && Array.isArray(this.agentCode.middleware)) {
|
|
583
|
+
this.agentCode.middleware.forEach((middleware) => {
|
|
584
|
+
if (typeof middleware === "function") {
|
|
585
|
+
this.httpApp.use(middleware);
|
|
586
|
+
logger.info("Registered custom middleware");
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get list of registered routes
|
|
594
|
+
*/
|
|
595
|
+
getRoutesList() {
|
|
596
|
+
if (!this.httpApp) return [];
|
|
597
|
+
|
|
598
|
+
const routes = [];
|
|
599
|
+
this.httpApp._router.stack.forEach((middleware) => {
|
|
600
|
+
if (middleware.route) {
|
|
601
|
+
const methods = Object.keys(middleware.route.methods);
|
|
602
|
+
routes.push({
|
|
603
|
+
path: middleware.route.path,
|
|
604
|
+
methods: methods.map((m) => m.toUpperCase()),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
return routes;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Start health check server
|
|
614
|
+
*/
|
|
615
|
+
startHealthServer() {
|
|
616
|
+
const port = process.env.HEALTH_PORT || 3001;
|
|
617
|
+
|
|
618
|
+
this.healthApp.listen(port, "0.0.0.0", () => {
|
|
619
|
+
logger.info(`Health server listening on port ${port}`);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Start HTTP server
|
|
625
|
+
*/
|
|
626
|
+
startHttpServer() {
|
|
627
|
+
if (!this.httpEnabled || !this.httpApp) return;
|
|
628
|
+
|
|
629
|
+
this.httpApp.listen(this.httpPort, this.httpHost, () => {
|
|
630
|
+
logger.info(
|
|
631
|
+
`🌐 HTTP server listening on ${this.httpHost}:${this.httpPort}`
|
|
632
|
+
);
|
|
633
|
+
logger.info(
|
|
634
|
+
`🔗 Agent HTTP endpoint: http://${this.httpHost}:${this.httpPort}`
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Setup direct prompting server (WebSocket/HTTP)
|
|
641
|
+
*/
|
|
642
|
+
async setupDirectPromptingServer() {
|
|
643
|
+
const directPromptingEnabled =
|
|
644
|
+
process.env.DIRECT_PROMPTING_ENABLED !== "false";
|
|
645
|
+
logger.info(`🔍 Direct prompting enabled: ${directPromptingEnabled}`);
|
|
646
|
+
if (!directPromptingEnabled) return;
|
|
647
|
+
|
|
648
|
+
const protocol = process.env.DIRECT_PROMPTING_PROTOCOL || "websocket";
|
|
649
|
+
const port = parseInt(process.env.DOCKER_PORT) || 3000;
|
|
650
|
+
|
|
651
|
+
logger.info(
|
|
652
|
+
`Setting up direct prompting server (${protocol}) on port ${port}`
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
if (protocol === "websocket") {
|
|
656
|
+
logger.info("📡 Setting up WebSocket server...");
|
|
657
|
+
await this.setupWebSocketServer(port);
|
|
658
|
+
} else if (protocol === "http") {
|
|
659
|
+
logger.info("🌐 Setting up HTTP prompting endpoints...");
|
|
660
|
+
await this.setupHttpPromptingEndpoints();
|
|
661
|
+
} else {
|
|
662
|
+
logger.warn(`⚠️ Unknown protocol: ${protocol}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
logger.info("✅ Direct prompting server setup completed");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Setup WebSocket server for direct prompting
|
|
670
|
+
*/
|
|
671
|
+
async setupWebSocketServer(port) {
|
|
672
|
+
const WebSocket = require("ws");
|
|
673
|
+
const maxConnections =
|
|
674
|
+
parseInt(process.env.DIRECT_PROMPTING_MAX_CONNECTIONS) || 100;
|
|
675
|
+
const authentication =
|
|
676
|
+
process.env.DIRECT_PROMPTING_AUTHENTICATION === "true";
|
|
677
|
+
|
|
678
|
+
this.wsServer = new WebSocket.Server({
|
|
679
|
+
port: port,
|
|
680
|
+
host: "0.0.0.0",
|
|
681
|
+
maxClients: maxConnections,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
this.activeConnections = 0;
|
|
685
|
+
|
|
686
|
+
logger.info(
|
|
687
|
+
`WebSocket server configured with max ${maxConnections} connections, auth: ${authentication}`
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
this.wsServer.on("connection", (ws, req) => {
|
|
691
|
+
const clientId = require("uuid").v4();
|
|
692
|
+
this.activeConnections++;
|
|
693
|
+
logger.info(
|
|
694
|
+
`WebSocket client connected: ${clientId} (${this.activeConnections}/${maxConnections})`
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
ws.on("message", async (message) => {
|
|
698
|
+
try {
|
|
699
|
+
const data = JSON.parse(message.toString());
|
|
700
|
+
const { prompt, conversationId, metadata } = data;
|
|
701
|
+
|
|
702
|
+
if (!prompt) {
|
|
703
|
+
ws.send(
|
|
704
|
+
JSON.stringify({
|
|
705
|
+
error: "Prompt is required",
|
|
706
|
+
timestamp: new Date().toISOString(),
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Process the prompt with LLM
|
|
713
|
+
const response = await this.processDirectPrompt(prompt, {
|
|
714
|
+
conversationId,
|
|
715
|
+
metadata,
|
|
716
|
+
clientId,
|
|
717
|
+
protocol: "websocket",
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Send response back
|
|
721
|
+
ws.send(
|
|
722
|
+
JSON.stringify({
|
|
723
|
+
response: response.content,
|
|
724
|
+
conversationId: conversationId || response.conversationId,
|
|
725
|
+
metadata: response.metadata,
|
|
726
|
+
timestamp: new Date().toISOString(),
|
|
727
|
+
})
|
|
728
|
+
);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
logger.error("WebSocket message processing error:", error);
|
|
731
|
+
ws.send(
|
|
732
|
+
JSON.stringify({
|
|
733
|
+
error: "Failed to process prompt",
|
|
734
|
+
message: error.message,
|
|
735
|
+
timestamp: new Date().toISOString(),
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
ws.on("close", () => {
|
|
742
|
+
this.activeConnections--;
|
|
743
|
+
logger.info(
|
|
744
|
+
`WebSocket client disconnected: ${clientId} (${this.activeConnections}/${maxConnections})`
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
ws.on("error", (error) => {
|
|
749
|
+
logger.error(`WebSocket error for client ${clientId}:`, error);
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
logger.info(`🔌 WebSocket server listening on port ${port}`);
|
|
754
|
+
logger.info(`🔗 Direct prompting endpoint: ws://localhost:${port}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Setup HTTP endpoints for direct prompting
|
|
759
|
+
*/
|
|
760
|
+
async setupHttpPromptingEndpoints() {
|
|
761
|
+
logger.info("🔧 Setting up HTTP prompting endpoints...");
|
|
762
|
+
const port = parseInt(process.env.DOCKER_PORT) || 3000;
|
|
763
|
+
logger.info(`Port: ${port}, httpApp exists: ${!!this.httpApp}`);
|
|
764
|
+
|
|
765
|
+
if (!this.httpApp) {
|
|
766
|
+
// Create a minimal HTTP app for prompting if main HTTP is disabled
|
|
767
|
+
logger.info("Creating new Express app for HTTP prompting");
|
|
768
|
+
this.httpApp = express();
|
|
769
|
+
this.httpApp.use(express.json({ limit: "10mb" }));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Direct prompting endpoint
|
|
773
|
+
this.httpApp.post("/prompt", async (req, res) => {
|
|
774
|
+
try {
|
|
775
|
+
const { prompt, conversationId, metadata } = req.body;
|
|
776
|
+
|
|
777
|
+
if (!prompt) {
|
|
778
|
+
return res.status(400).json({
|
|
779
|
+
error: "Prompt is required",
|
|
780
|
+
timestamp: new Date().toISOString(),
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const response = await this.processDirectPrompt(prompt, {
|
|
785
|
+
conversationId,
|
|
786
|
+
metadata,
|
|
787
|
+
protocol: "http",
|
|
788
|
+
clientIp: req.ip,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
res.json({
|
|
792
|
+
response: response.content,
|
|
793
|
+
conversationId: conversationId || response.conversationId,
|
|
794
|
+
metadata: response.metadata,
|
|
795
|
+
timestamp: new Date().toISOString(),
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
logger.error("HTTP prompt processing error:", error);
|
|
799
|
+
res.status(500).json({
|
|
800
|
+
error: "Failed to process prompt",
|
|
801
|
+
message: error.message,
|
|
802
|
+
timestamp: new Date().toISOString(),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// Start the HTTP server if it's not already running
|
|
808
|
+
logger.info(
|
|
809
|
+
`HTTP server status: httpEnabled=${this.httpEnabled}, port=${port}`
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
if (!this.httpEnabled) {
|
|
813
|
+
// Only start if main HTTP server is not enabled
|
|
814
|
+
logger.info(`Starting HTTP direct prompting server on port ${port}...`);
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const server = this.httpApp.listen(port, "0.0.0.0", () => {
|
|
818
|
+
logger.info(
|
|
819
|
+
`🌐 HTTP direct prompting server listening on port ${port}`
|
|
820
|
+
);
|
|
821
|
+
logger.info(
|
|
822
|
+
`📡 Direct prompting endpoint: POST http://localhost:${port}/prompt`
|
|
823
|
+
);
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
server.on("error", (error) => {
|
|
827
|
+
logger.error(`HTTP server error:`, error);
|
|
828
|
+
});
|
|
829
|
+
} catch (error) {
|
|
830
|
+
logger.error(`Failed to start HTTP server:`, error);
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
logger.info(`📡 HTTP prompting endpoint added: POST /prompt`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Process a direct prompt and emit events
|
|
839
|
+
*/
|
|
840
|
+
async processDirectPrompt(prompt, context = {}) {
|
|
841
|
+
const startTime = Date.now();
|
|
842
|
+
const conversationId = context.conversationId || require("uuid").v4();
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
// Emit request start event and allow handlers to modify the prompt
|
|
846
|
+
const startEventData = {
|
|
847
|
+
prompt,
|
|
848
|
+
conversationId,
|
|
849
|
+
context,
|
|
850
|
+
timestamp: new Date().toISOString(),
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const modifiedData = this.emitEventWithResponse("request_output:start", startEventData);
|
|
854
|
+
|
|
855
|
+
// Use modified prompt if handlers returned one, otherwise use original
|
|
856
|
+
const finalPrompt = modifiedData?.prompt || prompt;
|
|
857
|
+
|
|
858
|
+
// Process with LLM
|
|
859
|
+
let response;
|
|
860
|
+
if (this.llmProvider === "openai") {
|
|
861
|
+
const completion = await this.llmClient.chat.completions.create({
|
|
862
|
+
model: this.llmModel,
|
|
863
|
+
messages: [
|
|
864
|
+
{ role: "system", content: this.agentPrompt },
|
|
865
|
+
{ role: "user", content: finalPrompt },
|
|
866
|
+
],
|
|
867
|
+
temperature: parseFloat(process.env.LLM_TEMPERATURE) || 0.7,
|
|
868
|
+
max_tokens: parseInt(process.env.LLM_MAX_TOKENS) || 1000,
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
response = {
|
|
872
|
+
content: completion.choices[0].message.content,
|
|
873
|
+
usage: completion.usage,
|
|
874
|
+
model: completion.model,
|
|
875
|
+
};
|
|
876
|
+
} else {
|
|
877
|
+
// Fallback for other providers
|
|
878
|
+
response = {
|
|
879
|
+
content: `Echo: ${prompt} (LLM provider ${this.llmProvider} not fully implemented)`,
|
|
880
|
+
usage: { total_tokens: 0 },
|
|
881
|
+
model: this.llmModel,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const processingTime = Date.now() - startTime;
|
|
886
|
+
|
|
887
|
+
// Emit request output event
|
|
888
|
+
this.emitEvent("request_output", {
|
|
889
|
+
prompt,
|
|
890
|
+
finalPrompt, // Include the final prompt that was sent to LLM
|
|
891
|
+
response: response.content,
|
|
892
|
+
conversationId,
|
|
893
|
+
context,
|
|
894
|
+
usage: response.usage,
|
|
895
|
+
model: response.model,
|
|
896
|
+
processingTime,
|
|
897
|
+
promptModified: finalPrompt !== prompt, // Indicate if prompt was modified
|
|
898
|
+
timestamp: new Date().toISOString(),
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Emit end event and allow handlers to modify the response before returning
|
|
902
|
+
const endEventData = {
|
|
903
|
+
conversationId,
|
|
904
|
+
prompt,
|
|
905
|
+
finalPrompt,
|
|
906
|
+
response: response.content,
|
|
907
|
+
context,
|
|
908
|
+
usage: response.usage,
|
|
909
|
+
model: response.model,
|
|
910
|
+
processingTime,
|
|
911
|
+
promptModified: finalPrompt !== prompt,
|
|
912
|
+
success: true,
|
|
913
|
+
timestamp: new Date().toISOString(),
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const modifiedEndData = this.emitEventWithResponse("request_output:end", endEventData);
|
|
917
|
+
|
|
918
|
+
// Use the final response (potentially modified by handlers)
|
|
919
|
+
const finalResponse = modifiedEndData.response || response.content;
|
|
920
|
+
|
|
921
|
+
return {
|
|
922
|
+
content: finalResponse,
|
|
923
|
+
conversationId,
|
|
924
|
+
metadata: {
|
|
925
|
+
usage: response.usage,
|
|
926
|
+
model: response.model,
|
|
927
|
+
processingTime,
|
|
928
|
+
responseModified: finalResponse !== response.content, // Indicate if response was modified
|
|
929
|
+
},
|
|
930
|
+
};
|
|
931
|
+
} catch (error) {
|
|
932
|
+
const processingTime = Date.now() - startTime;
|
|
933
|
+
|
|
934
|
+
// Emit error event
|
|
935
|
+
this.emitEvent("request_output:error", {
|
|
936
|
+
prompt,
|
|
937
|
+
finalPrompt,
|
|
938
|
+
conversationId,
|
|
939
|
+
context,
|
|
940
|
+
error: error.message,
|
|
941
|
+
processingTime,
|
|
942
|
+
promptModified: finalPrompt !== prompt,
|
|
943
|
+
success: false,
|
|
944
|
+
timestamp: new Date().toISOString(),
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Wrap HTTP handler to emit tool events
|
|
953
|
+
*/
|
|
954
|
+
wrapHttpHandlerWithEvents(method, path, originalHandler) {
|
|
955
|
+
return async (req, res) => {
|
|
956
|
+
const startTime = Date.now();
|
|
957
|
+
const requestId = require("uuid").v4();
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
// Emit call event
|
|
961
|
+
this.emitEvent("tool:http-server:call", {
|
|
962
|
+
requestId,
|
|
963
|
+
method: method.toUpperCase(),
|
|
964
|
+
path,
|
|
965
|
+
headers: req.headers,
|
|
966
|
+
body: req.body,
|
|
967
|
+
query: req.query,
|
|
968
|
+
params: req.params,
|
|
969
|
+
timestamp: new Date().toISOString(),
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Emit specific method call event
|
|
973
|
+
this.emitEvent(`tool:http-server:call:${method.toLowerCase()}`, {
|
|
974
|
+
requestId,
|
|
975
|
+
path,
|
|
976
|
+
headers: req.headers,
|
|
977
|
+
body: req.body,
|
|
978
|
+
query: req.query,
|
|
979
|
+
params: req.params,
|
|
980
|
+
timestamp: new Date().toISOString(),
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Capture response data
|
|
984
|
+
const originalSend = res.send;
|
|
985
|
+
const originalJson = res.json;
|
|
986
|
+
let responseData = null;
|
|
987
|
+
let statusCode = 200;
|
|
988
|
+
|
|
989
|
+
res.send = function (data) {
|
|
990
|
+
responseData = data;
|
|
991
|
+
statusCode = res.statusCode;
|
|
992
|
+
return originalSend.call(this, data);
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
res.json = function (data) {
|
|
996
|
+
responseData = data;
|
|
997
|
+
statusCode = res.statusCode;
|
|
998
|
+
return originalJson.call(this, data);
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Execute original handler
|
|
1002
|
+
const result = await originalHandler(req, res);
|
|
1003
|
+
|
|
1004
|
+
const processingTime = Date.now() - startTime;
|
|
1005
|
+
|
|
1006
|
+
// Emit response event
|
|
1007
|
+
this.emitEvent("tool:http-server:response", {
|
|
1008
|
+
requestId,
|
|
1009
|
+
method: method.toUpperCase(),
|
|
1010
|
+
path,
|
|
1011
|
+
statusCode,
|
|
1012
|
+
responseData,
|
|
1013
|
+
processingTime,
|
|
1014
|
+
timestamp: new Date().toISOString(),
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// Emit specific method response event
|
|
1018
|
+
this.emitEvent(`tool:http-server:response:${method.toLowerCase()}`, {
|
|
1019
|
+
requestId,
|
|
1020
|
+
path,
|
|
1021
|
+
statusCode,
|
|
1022
|
+
responseData,
|
|
1023
|
+
processingTime,
|
|
1024
|
+
timestamp: new Date().toISOString(),
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// Emit wildcard events
|
|
1028
|
+
this.emitEvent("tool:http-server:*", {
|
|
1029
|
+
type: "response",
|
|
1030
|
+
requestId,
|
|
1031
|
+
method: method.toUpperCase(),
|
|
1032
|
+
path,
|
|
1033
|
+
statusCode,
|
|
1034
|
+
responseData,
|
|
1035
|
+
processingTime,
|
|
1036
|
+
timestamp: new Date().toISOString(),
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
return result;
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
const processingTime = Date.now() - startTime;
|
|
1042
|
+
|
|
1043
|
+
// Emit error event
|
|
1044
|
+
this.emitEvent("tool:http-server:error", {
|
|
1045
|
+
requestId,
|
|
1046
|
+
method: method.toUpperCase(),
|
|
1047
|
+
path,
|
|
1048
|
+
error: error.message,
|
|
1049
|
+
stack: error.stack,
|
|
1050
|
+
processingTime,
|
|
1051
|
+
timestamp: new Date().toISOString(),
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Create tools proxy for agent runtime
|
|
1061
|
+
*/
|
|
1062
|
+
createToolsProxy() {
|
|
1063
|
+
// Basic built-in tools that work in container environment
|
|
1064
|
+
return {
|
|
1065
|
+
httpRequest: async (params) => {
|
|
1066
|
+
const axios = require("axios");
|
|
1067
|
+
try {
|
|
1068
|
+
const response = await axios({
|
|
1069
|
+
url: params.url,
|
|
1070
|
+
method: params.method || "GET",
|
|
1071
|
+
headers: params.headers || {},
|
|
1072
|
+
data: params.data,
|
|
1073
|
+
timeout: params.timeout || 10000,
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
return {
|
|
1077
|
+
status: response.status,
|
|
1078
|
+
headers: response.headers,
|
|
1079
|
+
data: response.data,
|
|
1080
|
+
success: response.status >= 200 && response.status < 300,
|
|
1081
|
+
};
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
throw new Error(`HTTP request failed: ${error.message}`);
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
|
|
1087
|
+
getCurrentTime: (params = {}) => {
|
|
1088
|
+
const now = new Date();
|
|
1089
|
+
const format = params.format || "iso";
|
|
1090
|
+
|
|
1091
|
+
switch (format) {
|
|
1092
|
+
case "iso":
|
|
1093
|
+
return {
|
|
1094
|
+
formatted: now.toISOString(),
|
|
1095
|
+
timestamp: now.toISOString(),
|
|
1096
|
+
};
|
|
1097
|
+
case "unix":
|
|
1098
|
+
return {
|
|
1099
|
+
formatted: Math.floor(now.getTime() / 1000),
|
|
1100
|
+
timestamp: now.toISOString(),
|
|
1101
|
+
};
|
|
1102
|
+
case "readable":
|
|
1103
|
+
return {
|
|
1104
|
+
formatted: now.toLocaleString(),
|
|
1105
|
+
timestamp: now.toISOString(),
|
|
1106
|
+
};
|
|
1107
|
+
default:
|
|
1108
|
+
return {
|
|
1109
|
+
formatted: now.toISOString(),
|
|
1110
|
+
timestamp: now.toISOString(),
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
|
|
1115
|
+
analyzeText: (params) => {
|
|
1116
|
+
const text = params.text;
|
|
1117
|
+
const words = text.split(/\s+/).filter((word) => word.length > 0);
|
|
1118
|
+
const sentences = text
|
|
1119
|
+
.split(/[.!?]+/)
|
|
1120
|
+
.filter((s) => s.trim().length > 0);
|
|
1121
|
+
|
|
1122
|
+
const result = {
|
|
1123
|
+
length: text.length,
|
|
1124
|
+
stats: {
|
|
1125
|
+
characters: text.length,
|
|
1126
|
+
words: words.length,
|
|
1127
|
+
sentences: sentences.length,
|
|
1128
|
+
averageWordsPerSentence:
|
|
1129
|
+
sentences.length > 0
|
|
1130
|
+
? Math.round((words.length / sentences.length) * 10) / 10
|
|
1131
|
+
: 0,
|
|
1132
|
+
},
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
if (params.includeSentiment) {
|
|
1136
|
+
const positiveWords = [
|
|
1137
|
+
"good",
|
|
1138
|
+
"great",
|
|
1139
|
+
"excellent",
|
|
1140
|
+
"amazing",
|
|
1141
|
+
"wonderful",
|
|
1142
|
+
];
|
|
1143
|
+
const negativeWords = [
|
|
1144
|
+
"bad",
|
|
1145
|
+
"terrible",
|
|
1146
|
+
"awful",
|
|
1147
|
+
"horrible",
|
|
1148
|
+
"disappointing",
|
|
1149
|
+
];
|
|
1150
|
+
|
|
1151
|
+
const lowerText = text.toLowerCase();
|
|
1152
|
+
const positiveCount = positiveWords.filter((word) =>
|
|
1153
|
+
lowerText.includes(word)
|
|
1154
|
+
).length;
|
|
1155
|
+
const negativeCount = negativeWords.filter((word) =>
|
|
1156
|
+
lowerText.includes(word)
|
|
1157
|
+
).length;
|
|
1158
|
+
|
|
1159
|
+
result.sentiment = {
|
|
1160
|
+
score: positiveCount - negativeCount,
|
|
1161
|
+
label:
|
|
1162
|
+
positiveCount > negativeCount
|
|
1163
|
+
? "positive"
|
|
1164
|
+
: negativeCount > positiveCount
|
|
1165
|
+
? "negative"
|
|
1166
|
+
: "neutral",
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return result;
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Graceful shutdown
|
|
1177
|
+
*/
|
|
1178
|
+
async shutdown() {
|
|
1179
|
+
logger.info("Shutting down agent...");
|
|
1180
|
+
|
|
1181
|
+
this.isRunning = false;
|
|
1182
|
+
|
|
1183
|
+
// Clean up keep-alive mechanisms
|
|
1184
|
+
if (this.heartbeatInterval) {
|
|
1185
|
+
clearInterval(this.heartbeatInterval);
|
|
1186
|
+
logger.debug("Heartbeat interval cleared");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (this.keepAliveTimeout) {
|
|
1190
|
+
clearTimeout(this.keepAliveTimeout);
|
|
1191
|
+
logger.debug("Keep-alive timeout cleared");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Call shutdown handlers if they exist
|
|
1195
|
+
const shutdownHandlers = this.handlers.get("shutdown") || [];
|
|
1196
|
+
for (const handler of shutdownHandlers) {
|
|
1197
|
+
try {
|
|
1198
|
+
await handler();
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
logger.error("Error in shutdown handler:", error);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
logger.info("Agent shutdown complete");
|
|
1205
|
+
process.exit(0);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Handle shutdown signals
|
|
1210
|
+
process.on("SIGTERM", async () => {
|
|
1211
|
+
if (runtime) {
|
|
1212
|
+
await runtime.shutdown();
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
process.on("SIGINT", async () => {
|
|
1217
|
+
if (runtime) {
|
|
1218
|
+
await runtime.shutdown();
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Start the agent runtime
|
|
1223
|
+
const runtime = new AgentRuntime();
|
|
1224
|
+
runtime.initialize().catch((error) => {
|
|
1225
|
+
logger.error("Failed to start agent:", error);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
});
|