@loxia-labs/loxia-autopilot-one 1.0.1
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/LICENSE +267 -0
- package/README.md +509 -0
- package/bin/cli.js +117 -0
- package/package.json +94 -0
- package/scripts/install-scanners.js +236 -0
- package/src/analyzers/CSSAnalyzer.js +297 -0
- package/src/analyzers/ConfigValidator.js +690 -0
- package/src/analyzers/ESLintAnalyzer.js +320 -0
- package/src/analyzers/JavaScriptAnalyzer.js +261 -0
- package/src/analyzers/PrettierFormatter.js +247 -0
- package/src/analyzers/PythonAnalyzer.js +266 -0
- package/src/analyzers/SecurityAnalyzer.js +729 -0
- package/src/analyzers/TypeScriptAnalyzer.js +247 -0
- package/src/analyzers/codeCloneDetector/analyzer.js +344 -0
- package/src/analyzers/codeCloneDetector/detector.js +203 -0
- package/src/analyzers/codeCloneDetector/index.js +160 -0
- package/src/analyzers/codeCloneDetector/parser.js +199 -0
- package/src/analyzers/codeCloneDetector/reporter.js +148 -0
- package/src/analyzers/codeCloneDetector/scanner.js +59 -0
- package/src/core/agentPool.js +1474 -0
- package/src/core/agentScheduler.js +2147 -0
- package/src/core/contextManager.js +709 -0
- package/src/core/messageProcessor.js +732 -0
- package/src/core/orchestrator.js +548 -0
- package/src/core/stateManager.js +877 -0
- package/src/index.js +631 -0
- package/src/interfaces/cli.js +549 -0
- package/src/interfaces/webServer.js +2162 -0
- package/src/modules/fileExplorer/controller.js +280 -0
- package/src/modules/fileExplorer/index.js +37 -0
- package/src/modules/fileExplorer/middleware.js +92 -0
- package/src/modules/fileExplorer/routes.js +125 -0
- package/src/modules/fileExplorer/types.js +44 -0
- package/src/services/aiService.js +1232 -0
- package/src/services/apiKeyManager.js +164 -0
- package/src/services/benchmarkService.js +366 -0
- package/src/services/budgetService.js +539 -0
- package/src/services/contextInjectionService.js +247 -0
- package/src/services/conversationCompactionService.js +637 -0
- package/src/services/errorHandler.js +810 -0
- package/src/services/fileAttachmentService.js +544 -0
- package/src/services/modelRouterService.js +366 -0
- package/src/services/modelsService.js +322 -0
- package/src/services/qualityInspector.js +796 -0
- package/src/services/tokenCountingService.js +536 -0
- package/src/tools/agentCommunicationTool.js +1344 -0
- package/src/tools/agentDelayTool.js +485 -0
- package/src/tools/asyncToolManager.js +604 -0
- package/src/tools/baseTool.js +800 -0
- package/src/tools/browserTool.js +920 -0
- package/src/tools/cloneDetectionTool.js +621 -0
- package/src/tools/dependencyResolverTool.js +1215 -0
- package/src/tools/fileContentReplaceTool.js +875 -0
- package/src/tools/fileSystemTool.js +1107 -0
- package/src/tools/fileTreeTool.js +853 -0
- package/src/tools/imageTool.js +901 -0
- package/src/tools/importAnalyzerTool.js +1060 -0
- package/src/tools/jobDoneTool.js +248 -0
- package/src/tools/seekTool.js +956 -0
- package/src/tools/staticAnalysisTool.js +1778 -0
- package/src/tools/taskManagerTool.js +2873 -0
- package/src/tools/terminalTool.js +2304 -0
- package/src/tools/webTool.js +1430 -0
- package/src/types/agent.js +519 -0
- package/src/types/contextReference.js +972 -0
- package/src/types/conversation.js +730 -0
- package/src/types/toolCommand.js +747 -0
- package/src/utilities/attachmentValidator.js +292 -0
- package/src/utilities/configManager.js +582 -0
- package/src/utilities/constants.js +722 -0
- package/src/utilities/directoryAccessManager.js +535 -0
- package/src/utilities/fileProcessor.js +307 -0
- package/src/utilities/logger.js +436 -0
- package/src/utilities/tagParser.js +1246 -0
- package/src/utilities/toolConstants.js +317 -0
- package/web-ui/build/index.html +15 -0
- package/web-ui/build/logo.png +0 -0
- package/web-ui/build/logo2.png +0 -0
- package/web-ui/build/static/index-CjkkcnFA.js +344 -0
- package/web-ui/build/static/index-Dy2bYbOa.css +1 -0
|
@@ -0,0 +1,2162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Server - HTTP and WebSocket server for web interface
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Serve React frontend application
|
|
6
|
+
* - Handle HTTP API requests
|
|
7
|
+
* - Manage WebSocket connections for real-time updates
|
|
8
|
+
* - File upload and project management
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import { createServer } from 'http';
|
|
13
|
+
import { WebSocketServer } from 'ws';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { promises as fs } from 'fs';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
INTERFACE_TYPES,
|
|
20
|
+
ORCHESTRATOR_ACTIONS,
|
|
21
|
+
HTTP_STATUS,
|
|
22
|
+
AGENT_MODES
|
|
23
|
+
} from '../utilities/constants.js';
|
|
24
|
+
|
|
25
|
+
// Import file explorer module
|
|
26
|
+
import { initFileExplorerModule } from '../modules/fileExplorer/index.js';
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = path.dirname(__filename);
|
|
30
|
+
|
|
31
|
+
class WebServer {
|
|
32
|
+
constructor(orchestrator, logger, config = {}) {
|
|
33
|
+
this.orchestrator = orchestrator;
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
this.config = config;
|
|
36
|
+
|
|
37
|
+
this.port = config.port || 3000;
|
|
38
|
+
this.host = config.host || 'localhost';
|
|
39
|
+
|
|
40
|
+
// Express app
|
|
41
|
+
this.app = express();
|
|
42
|
+
this.server = createServer(this.app);
|
|
43
|
+
|
|
44
|
+
// WebSocket server with CORS support
|
|
45
|
+
this.wss = new WebSocketServer({
|
|
46
|
+
server: this.server,
|
|
47
|
+
// Allow all origins for WebSocket connections
|
|
48
|
+
verifyClient: (info) => {
|
|
49
|
+
// Log connection attempt for debugging
|
|
50
|
+
this.logger?.info('WebSocket connection attempt', {
|
|
51
|
+
origin: info.origin,
|
|
52
|
+
host: info.req.headers.host,
|
|
53
|
+
userAgent: info.req.headers['user-agent'],
|
|
54
|
+
url: info.req.url
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Allow all origins (you can restrict this later if needed)
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Active WebSocket connections
|
|
63
|
+
this.connections = new Map();
|
|
64
|
+
|
|
65
|
+
// Session management
|
|
66
|
+
this.sessions = new Map();
|
|
67
|
+
|
|
68
|
+
// API Key Manager reference (will be set by LoxiaSystem)
|
|
69
|
+
this.apiKeyManager = null;
|
|
70
|
+
|
|
71
|
+
this.isRunning = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Initialize web server
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
async initialize() {
|
|
79
|
+
try {
|
|
80
|
+
this.setupMiddleware();
|
|
81
|
+
this.setupRoutes();
|
|
82
|
+
this.setupWebSocket();
|
|
83
|
+
|
|
84
|
+
await this.startServer();
|
|
85
|
+
|
|
86
|
+
this.logger.info('Web server initialized', {
|
|
87
|
+
port: this.port,
|
|
88
|
+
host: this.host,
|
|
89
|
+
url: `http://${this.host}:${this.port}`
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.logger.error('Web server initialization failed', {
|
|
94
|
+
error: error.message,
|
|
95
|
+
stack: error.stack
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Setup Express middleware
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
setupMiddleware() {
|
|
106
|
+
// CORS middleware
|
|
107
|
+
this.app.use((req, res, next) => {
|
|
108
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
109
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
110
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
111
|
+
|
|
112
|
+
if (req.method === 'OPTIONS') {
|
|
113
|
+
res.sendStatus(HTTP_STATUS.OK);
|
|
114
|
+
} else {
|
|
115
|
+
next();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// JSON parsing
|
|
120
|
+
this.app.use(express.json({ limit: '10mb' }));
|
|
121
|
+
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
122
|
+
|
|
123
|
+
// Initialize file explorer module
|
|
124
|
+
this.fileExplorerModule = initFileExplorerModule({
|
|
125
|
+
showHidden: false,
|
|
126
|
+
restrictedPaths: [] // Can be configured as needed
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Mount file explorer routes
|
|
130
|
+
this.app.use('/api/file-explorer', this.fileExplorerModule.router);
|
|
131
|
+
|
|
132
|
+
// Static files (React build)
|
|
133
|
+
const staticPath = path.join(__dirname, '../../web-ui/build');
|
|
134
|
+
this.app.use(express.static(staticPath));
|
|
135
|
+
|
|
136
|
+
// Request logging
|
|
137
|
+
this.app.use((req, res, next) => {
|
|
138
|
+
this.logger.debug(`${req.method} ${req.path}`, {
|
|
139
|
+
ip: req.ip,
|
|
140
|
+
userAgent: req.get('User-Agent')
|
|
141
|
+
});
|
|
142
|
+
next();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Setup API routes
|
|
148
|
+
* @private
|
|
149
|
+
*/
|
|
150
|
+
setupRoutes() {
|
|
151
|
+
// Health check
|
|
152
|
+
this.app.get('/api/health', async (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
155
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
156
|
+
|
|
157
|
+
res.json({
|
|
158
|
+
status: 'healthy',
|
|
159
|
+
version: packageJson.version || '1.0.0',
|
|
160
|
+
timestamp: new Date().toISOString()
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
res.json({
|
|
164
|
+
status: 'healthy',
|
|
165
|
+
version: '1.0.0',
|
|
166
|
+
timestamp: new Date().toISOString()
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Session creation
|
|
172
|
+
this.app.post('/api/sessions', async (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
const sessionId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
175
|
+
const projectDir = req.body.projectDir || process.cwd();
|
|
176
|
+
|
|
177
|
+
const session = {
|
|
178
|
+
id: sessionId,
|
|
179
|
+
projectDir,
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
lastActivity: new Date().toISOString()
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
this.sessions.set(sessionId, session);
|
|
185
|
+
|
|
186
|
+
res.json({
|
|
187
|
+
success: true,
|
|
188
|
+
session
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
} catch (error) {
|
|
192
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
193
|
+
success: false,
|
|
194
|
+
error: error.message
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Orchestrator API proxy
|
|
200
|
+
this.app.post('/api/orchestrator', async (req, res) => {
|
|
201
|
+
try {
|
|
202
|
+
const request = {
|
|
203
|
+
interface: INTERFACE_TYPES.WEB,
|
|
204
|
+
sessionId: req.body.sessionId,
|
|
205
|
+
action: req.body.action,
|
|
206
|
+
payload: req.body.payload,
|
|
207
|
+
projectDir: req.body.projectDir || process.cwd()
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const response = await this.orchestrator.processRequest(request);
|
|
211
|
+
|
|
212
|
+
// Broadcast updates via WebSocket
|
|
213
|
+
this.broadcastToSession(request.sessionId, {
|
|
214
|
+
type: 'orchestrator_response',
|
|
215
|
+
action: request.action,
|
|
216
|
+
response
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
res.json(response);
|
|
220
|
+
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.logger.error('Orchestrator API error', {
|
|
223
|
+
error: error.message,
|
|
224
|
+
body: req.body
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
228
|
+
success: false,
|
|
229
|
+
error: error.message
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// File operations
|
|
235
|
+
this.app.get('/api/files', async (req, res) => {
|
|
236
|
+
try {
|
|
237
|
+
const { path: filePath, projectDir } = req.query;
|
|
238
|
+
const fullPath = path.resolve(projectDir || process.cwd(), filePath || '.');
|
|
239
|
+
|
|
240
|
+
const stats = await fs.stat(fullPath);
|
|
241
|
+
|
|
242
|
+
if (stats.isDirectory()) {
|
|
243
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
244
|
+
const files = entries.map(entry => ({
|
|
245
|
+
name: entry.name,
|
|
246
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
247
|
+
path: path.join(filePath || '.', entry.name)
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
res.json({ success: true, files });
|
|
251
|
+
} else {
|
|
252
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
253
|
+
res.json({ success: true, content, type: 'file' });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
} catch (error) {
|
|
257
|
+
res.status(HTTP_STATUS.NOT_FOUND).json({
|
|
258
|
+
success: false,
|
|
259
|
+
error: error.message
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// File upload
|
|
265
|
+
this.app.post('/api/files/upload', async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const { fileName, content, projectDir } = req.body;
|
|
268
|
+
const targetDir = projectDir || process.cwd();
|
|
269
|
+
const fullPath = path.resolve(targetDir, fileName);
|
|
270
|
+
|
|
271
|
+
// Ensure the directory exists
|
|
272
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
273
|
+
|
|
274
|
+
// Write the file
|
|
275
|
+
await fs.writeFile(fullPath, content, 'utf8');
|
|
276
|
+
|
|
277
|
+
res.json({
|
|
278
|
+
success: true,
|
|
279
|
+
message: 'File uploaded successfully',
|
|
280
|
+
path: fullPath
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
} catch (error) {
|
|
284
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
285
|
+
success: false,
|
|
286
|
+
error: error.message
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Enhanced folder explorer endpoint
|
|
292
|
+
this.app.get('/api/explorer', async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const { path: requestedPath, showHidden = false } = req.query;
|
|
295
|
+
const basePath = requestedPath || process.cwd();
|
|
296
|
+
const fullPath = path.resolve(basePath);
|
|
297
|
+
|
|
298
|
+
// Security: Basic path traversal protection
|
|
299
|
+
const normalizedPath = path.normalize(fullPath);
|
|
300
|
+
|
|
301
|
+
const stats = await fs.stat(normalizedPath);
|
|
302
|
+
|
|
303
|
+
if (!stats.isDirectory()) {
|
|
304
|
+
return res.status(HTTP_STATUS.BAD_REQUEST).json({
|
|
305
|
+
success: false,
|
|
306
|
+
error: 'Path is not a directory'
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const entries = await fs.readdir(normalizedPath, { withFileTypes: true });
|
|
311
|
+
|
|
312
|
+
// Process entries
|
|
313
|
+
const items = await Promise.all(
|
|
314
|
+
entries
|
|
315
|
+
.filter(entry => showHidden === 'true' || !entry.name.startsWith('.'))
|
|
316
|
+
.map(async (entry) => {
|
|
317
|
+
const itemPath = path.join(normalizedPath, entry.name);
|
|
318
|
+
const itemStats = await fs.stat(itemPath).catch(() => null);
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
name: entry.name,
|
|
322
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
323
|
+
path: itemPath,
|
|
324
|
+
relativePath: path.relative(process.cwd(), itemPath),
|
|
325
|
+
size: itemStats?.size || 0,
|
|
326
|
+
modified: itemStats?.mtime || null,
|
|
327
|
+
isHidden: entry.name.startsWith('.'),
|
|
328
|
+
permissions: {
|
|
329
|
+
readable: true, // Could check with fs.access
|
|
330
|
+
writable: true // Could check with fs.access
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// Sort: directories first, then files, both alphabetically
|
|
337
|
+
items.sort((a, b) => {
|
|
338
|
+
if (a.type !== b.type) {
|
|
339
|
+
return a.type === 'directory' ? -1 : 1;
|
|
340
|
+
}
|
|
341
|
+
return a.name.localeCompare(b.name);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Get parent directory info
|
|
345
|
+
const parentPath = path.dirname(normalizedPath);
|
|
346
|
+
const hasParent = parentPath !== normalizedPath;
|
|
347
|
+
|
|
348
|
+
res.json({
|
|
349
|
+
success: true,
|
|
350
|
+
currentPath: normalizedPath,
|
|
351
|
+
currentRelativePath: path.relative(process.cwd(), normalizedPath),
|
|
352
|
+
parentPath: hasParent ? parentPath : null,
|
|
353
|
+
items,
|
|
354
|
+
totalItems: items.length,
|
|
355
|
+
directories: items.filter(item => item.type === 'directory').length,
|
|
356
|
+
files: items.filter(item => item.type === 'file').length
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
} catch (error) {
|
|
360
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
361
|
+
success: false,
|
|
362
|
+
error: error.message,
|
|
363
|
+
code: error.code
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Create directory endpoint
|
|
369
|
+
this.app.post('/api/explorer/mkdir', async (req, res) => {
|
|
370
|
+
try {
|
|
371
|
+
const { path: dirPath, name } = req.body;
|
|
372
|
+
const fullPath = path.resolve(dirPath, name);
|
|
373
|
+
|
|
374
|
+
await fs.mkdir(fullPath, { recursive: false });
|
|
375
|
+
|
|
376
|
+
res.json({
|
|
377
|
+
success: true,
|
|
378
|
+
path: fullPath,
|
|
379
|
+
relativePath: path.relative(process.cwd(), fullPath)
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
} catch (error) {
|
|
383
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
384
|
+
success: false,
|
|
385
|
+
error: error.message,
|
|
386
|
+
code: error.code
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Get directory info endpoint
|
|
392
|
+
this.app.get('/api/explorer/info', async (req, res) => {
|
|
393
|
+
try {
|
|
394
|
+
const { path: requestedPath } = req.query;
|
|
395
|
+
const fullPath = path.resolve(requestedPath);
|
|
396
|
+
|
|
397
|
+
const stats = await fs.stat(fullPath);
|
|
398
|
+
|
|
399
|
+
res.json({
|
|
400
|
+
success: true,
|
|
401
|
+
path: fullPath,
|
|
402
|
+
relativePath: path.relative(process.cwd(), fullPath),
|
|
403
|
+
isDirectory: stats.isDirectory(),
|
|
404
|
+
isFile: stats.isFile(),
|
|
405
|
+
size: stats.size,
|
|
406
|
+
created: stats.birthtime,
|
|
407
|
+
modified: stats.mtime,
|
|
408
|
+
accessed: stats.atime
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
} catch (error) {
|
|
412
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
413
|
+
success: false,
|
|
414
|
+
error: error.message,
|
|
415
|
+
code: error.code
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// LLM Chat endpoint - proxy to Loxia Azure backend
|
|
421
|
+
this.app.post('/api/llm/chat', async (req, res) => {
|
|
422
|
+
try {
|
|
423
|
+
const { sessionId, model, platformProvided } = req.body;
|
|
424
|
+
|
|
425
|
+
if (!sessionId) {
|
|
426
|
+
return res.status(400).json({
|
|
427
|
+
error: 'Session ID is required'
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Get API keys from session-based storage
|
|
432
|
+
let apiKey = null;
|
|
433
|
+
let vendorApiKey = null;
|
|
434
|
+
|
|
435
|
+
if (this.apiKeyManager) {
|
|
436
|
+
const keys = this.apiKeyManager.getKeysForRequest(sessionId, {
|
|
437
|
+
platformProvided: platformProvided || false,
|
|
438
|
+
vendor: this._getVendorFromModel(model)
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
apiKey = keys.loxiaApiKey;
|
|
442
|
+
vendorApiKey = keys.vendorApiKey;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Fallback to config/environment if no session keys
|
|
446
|
+
if (!apiKey) {
|
|
447
|
+
apiKey = this.config.loxiaApiKey || process.env.LOXIA_API_KEY;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Also try API key from request body for backward compatibility
|
|
451
|
+
if (!apiKey && req.body.apiKey) {
|
|
452
|
+
apiKey = req.body.apiKey;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (!apiKey) {
|
|
456
|
+
this.logger.warn('No API key available for chat request', {
|
|
457
|
+
sessionId,
|
|
458
|
+
model,
|
|
459
|
+
platformProvided,
|
|
460
|
+
hasSessionManager: !!this.apiKeyManager
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return res.status(401).json({
|
|
464
|
+
error: 'No API key configured. Please configure your Loxia API key in Settings.'
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Make request to Loxia Azure backend
|
|
469
|
+
const azureBackendUrl = 'https://autopilot-api.azurewebsites.net/llm/chat';
|
|
470
|
+
|
|
471
|
+
// Prepare request payload with API keys
|
|
472
|
+
const requestPayload = {
|
|
473
|
+
...req.body,
|
|
474
|
+
apiKey, // Loxia platform API key
|
|
475
|
+
...(vendorApiKey && { vendorApiKey }) // Include vendor key if available
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const fetchOptions = {
|
|
479
|
+
method: 'POST',
|
|
480
|
+
headers: {
|
|
481
|
+
'Content-Type': 'application/json',
|
|
482
|
+
'Authorization': `Bearer ${apiKey}`
|
|
483
|
+
},
|
|
484
|
+
body: JSON.stringify(requestPayload)
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
this.logger.info('Proxying chat request to Loxia Azure backend', {
|
|
488
|
+
url: azureBackendUrl,
|
|
489
|
+
model: req.body.model,
|
|
490
|
+
hasApiKey: !!apiKey
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const response = await fetch(azureBackendUrl, fetchOptions);
|
|
494
|
+
|
|
495
|
+
if (response.ok) {
|
|
496
|
+
const data = await response.json();
|
|
497
|
+
this.logger.info('Successfully received response from Azure backend', {
|
|
498
|
+
model: data.model,
|
|
499
|
+
contentLength: data.content?.length || 0
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
res.json(data);
|
|
503
|
+
} else {
|
|
504
|
+
const errorText = await response.text();
|
|
505
|
+
this.logger.warn('Azure backend chat request failed', {
|
|
506
|
+
status: response.status,
|
|
507
|
+
statusText: response.statusText,
|
|
508
|
+
error: errorText
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
res.status(response.status).json({
|
|
512
|
+
error: `Azure backend error: ${response.status} ${response.statusText}`,
|
|
513
|
+
details: errorText
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
} catch (error) {
|
|
518
|
+
this.logger.error('Failed to proxy chat request to Azure backend', {
|
|
519
|
+
error: error.message,
|
|
520
|
+
stack: error.stack
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
res.status(500).json({
|
|
524
|
+
error: 'Failed to connect to AI service',
|
|
525
|
+
details: error.message
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// LLM Models endpoint - proxy to Loxia Azure backend
|
|
531
|
+
this.app.get('/api/llm/models', async (req, res) => {
|
|
532
|
+
try {
|
|
533
|
+
// Get API key from authorization header if provided
|
|
534
|
+
const authHeader = req.headers.authorization;
|
|
535
|
+
let apiKey = null;
|
|
536
|
+
|
|
537
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
538
|
+
apiKey = authHeader.substring(7);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// If no API key, try to get from config or environment
|
|
542
|
+
if (!apiKey) {
|
|
543
|
+
apiKey = this.config.loxiaApiKey || process.env.LOXIA_API_KEY;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Make request to Loxia Azure backend
|
|
547
|
+
const azureBackendUrl = 'https://autopilot-api.azurewebsites.net/llm/models';
|
|
548
|
+
|
|
549
|
+
const fetchOptions = {
|
|
550
|
+
method: 'GET',
|
|
551
|
+
headers: {
|
|
552
|
+
'Content-Type': 'application/json',
|
|
553
|
+
...(apiKey && { 'Authorization': `Bearer ${apiKey}` })
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
this.logger.info('Fetching models from Loxia Azure backend', {
|
|
558
|
+
url: azureBackendUrl,
|
|
559
|
+
hasApiKey: !!apiKey
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const response = await fetch(azureBackendUrl, fetchOptions);
|
|
563
|
+
|
|
564
|
+
if (response.ok) {
|
|
565
|
+
const data = await response.json();
|
|
566
|
+
this.logger.info('Successfully fetched models from Azure backend', {
|
|
567
|
+
modelCount: data.models?.length || 0
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
res.json(data);
|
|
571
|
+
} else {
|
|
572
|
+
// If authentication failed or other error, return fallback models
|
|
573
|
+
const errorText = await response.text();
|
|
574
|
+
this.logger.warn('Azure backend request failed, using fallback models', {
|
|
575
|
+
status: response.status,
|
|
576
|
+
statusText: response.statusText,
|
|
577
|
+
error: errorText
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
res.json({
|
|
581
|
+
models: this.getDefaultModels(),
|
|
582
|
+
total: this.getDefaultModels().length,
|
|
583
|
+
fallback: true,
|
|
584
|
+
reason: `Azure backend error: ${response.status} ${response.statusText}`
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
} catch (error) {
|
|
589
|
+
this.logger.error('Failed to fetch models from Azure backend', {
|
|
590
|
+
error: error.message,
|
|
591
|
+
stack: error.stack
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Return fallback models on network or other errors
|
|
595
|
+
res.json({
|
|
596
|
+
models: this.getDefaultModels(),
|
|
597
|
+
total: this.getDefaultModels().length,
|
|
598
|
+
fallback: true,
|
|
599
|
+
error: error.message
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Tools information endpoint - get available tools from registry
|
|
605
|
+
this.app.get('/api/tools', async (req, res) => {
|
|
606
|
+
try {
|
|
607
|
+
// Get tools registry (passed from LoxiaSystem)
|
|
608
|
+
const toolsRegistry = this.toolsRegistry;
|
|
609
|
+
|
|
610
|
+
if (!toolsRegistry) {
|
|
611
|
+
return res.status(500).json({
|
|
612
|
+
error: 'Tools registry not available'
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Get available tools for UI
|
|
617
|
+
const tools = toolsRegistry.getAvailableToolsForUI();
|
|
618
|
+
|
|
619
|
+
this.logger.info('Serving tools information', {
|
|
620
|
+
toolCount: tools.length,
|
|
621
|
+
tools: tools.map(t => ({ id: t.id, name: t.name, category: t.category }))
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
res.json({
|
|
625
|
+
success: true,
|
|
626
|
+
tools,
|
|
627
|
+
total: tools.length
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this.logger.error('Failed to get tools information', {
|
|
632
|
+
error: error.message,
|
|
633
|
+
stack: error.stack
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
res.status(500).json({
|
|
637
|
+
error: 'Failed to retrieve tools information',
|
|
638
|
+
message: error.message
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// API Key Management Endpoints
|
|
644
|
+
|
|
645
|
+
// Set API keys for current session
|
|
646
|
+
this.app.post('/api/keys', async (req, res) => {
|
|
647
|
+
try {
|
|
648
|
+
const { sessionId, loxiaApiKey, vendorKeys } = req.body;
|
|
649
|
+
|
|
650
|
+
if (!sessionId) {
|
|
651
|
+
return res.status(400).json({
|
|
652
|
+
success: false,
|
|
653
|
+
error: 'Session ID is required'
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (!this.apiKeyManager) {
|
|
658
|
+
return res.status(500).json({
|
|
659
|
+
success: false,
|
|
660
|
+
error: 'API key manager not available'
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Set API keys for the session
|
|
665
|
+
this.apiKeyManager.setSessionKeys(sessionId, {
|
|
666
|
+
loxiaApiKey,
|
|
667
|
+
vendorKeys: vendorKeys || {}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
this.logger.info('API keys updated for session', {
|
|
671
|
+
sessionId,
|
|
672
|
+
hasLoxiaKey: !!loxiaApiKey,
|
|
673
|
+
vendorKeys: Object.keys(vendorKeys || {})
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Refresh models with the new API key context
|
|
677
|
+
if (this.orchestrator && this.orchestrator.modelsService && loxiaApiKey) {
|
|
678
|
+
try {
|
|
679
|
+
await this.orchestrator.modelsService.refresh({ sessionId, apiKey: loxiaApiKey });
|
|
680
|
+
this.logger.info('Models refreshed with new API key', { sessionId });
|
|
681
|
+
} catch (error) {
|
|
682
|
+
this.logger.warn('Failed to refresh models after API key update', {
|
|
683
|
+
error: error.message,
|
|
684
|
+
sessionId
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
res.json({
|
|
690
|
+
success: true,
|
|
691
|
+
message: 'API keys updated successfully',
|
|
692
|
+
sessionId,
|
|
693
|
+
hasLoxiaKey: !!loxiaApiKey,
|
|
694
|
+
vendorKeys: Object.keys(vendorKeys || {})
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
} catch (error) {
|
|
698
|
+
this.logger.error('Failed to set API keys', {
|
|
699
|
+
error: error.message,
|
|
700
|
+
sessionId: req.body.sessionId
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
res.status(500).json({
|
|
704
|
+
success: false,
|
|
705
|
+
error: error.message
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Get API keys for current session (returns only presence, not values)
|
|
711
|
+
this.app.get('/api/keys/:sessionId', async (req, res) => {
|
|
712
|
+
try {
|
|
713
|
+
const { sessionId } = req.params;
|
|
714
|
+
|
|
715
|
+
if (!this.apiKeyManager) {
|
|
716
|
+
return res.status(500).json({
|
|
717
|
+
success: false,
|
|
718
|
+
error: 'API key manager not available'
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const keys = this.apiKeyManager.getSessionKeys(sessionId);
|
|
723
|
+
|
|
724
|
+
// Return key presence only, not actual values
|
|
725
|
+
res.json({
|
|
726
|
+
success: true,
|
|
727
|
+
sessionId,
|
|
728
|
+
hasLoxiaKey: !!keys.loxiaApiKey,
|
|
729
|
+
vendorKeys: Object.keys(keys.vendorKeys || {}),
|
|
730
|
+
updatedAt: keys.updatedAt
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
} catch (error) {
|
|
734
|
+
this.logger.error('Failed to get API key status', {
|
|
735
|
+
error: error.message,
|
|
736
|
+
sessionId: req.params.sessionId
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
res.status(500).json({
|
|
740
|
+
success: false,
|
|
741
|
+
error: error.message
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Remove API keys for current session
|
|
747
|
+
this.app.delete('/api/keys/:sessionId', async (req, res) => {
|
|
748
|
+
try {
|
|
749
|
+
const { sessionId } = req.params;
|
|
750
|
+
|
|
751
|
+
if (!this.apiKeyManager) {
|
|
752
|
+
return res.status(500).json({
|
|
753
|
+
success: false,
|
|
754
|
+
error: 'API key manager not available'
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const removed = this.apiKeyManager.removeSessionKeys(sessionId);
|
|
759
|
+
|
|
760
|
+
res.json({
|
|
761
|
+
success: true,
|
|
762
|
+
removed,
|
|
763
|
+
message: removed ? 'API keys removed successfully' : 'No API keys found for session'
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
} catch (error) {
|
|
767
|
+
this.logger.error('Failed to remove API keys', {
|
|
768
|
+
error: error.message,
|
|
769
|
+
sessionId: req.params.sessionId
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
res.status(500).json({
|
|
773
|
+
success: false,
|
|
774
|
+
error: error.message
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Get active sessions with API keys (admin endpoint)
|
|
780
|
+
this.app.get('/api/keys', async (req, res) => {
|
|
781
|
+
try {
|
|
782
|
+
if (!this.apiKeyManager) {
|
|
783
|
+
return res.status(500).json({
|
|
784
|
+
success: false,
|
|
785
|
+
error: 'API key manager not available'
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const activeSessions = this.apiKeyManager.getActiveSessions();
|
|
790
|
+
|
|
791
|
+
res.json({
|
|
792
|
+
success: true,
|
|
793
|
+
sessions: activeSessions,
|
|
794
|
+
total: activeSessions.length
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
} catch (error) {
|
|
798
|
+
this.logger.error('Failed to get active sessions', {
|
|
799
|
+
error: error.message
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
res.status(500).json({
|
|
803
|
+
success: false,
|
|
804
|
+
error: error.message
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// File Attachments Management Endpoints
|
|
810
|
+
|
|
811
|
+
// Upload file attachment for agent
|
|
812
|
+
this.app.post('/api/agents/:agentId/attachments/upload', async (req, res) => {
|
|
813
|
+
try {
|
|
814
|
+
const { agentId } = req.params;
|
|
815
|
+
const { filePath, mode, fileName } = req.body;
|
|
816
|
+
|
|
817
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
818
|
+
return res.status(500).json({
|
|
819
|
+
success: false,
|
|
820
|
+
error: 'File attachment service not available'
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const result = await this.orchestrator.fileAttachmentService.uploadFile({
|
|
825
|
+
agentId,
|
|
826
|
+
filePath,
|
|
827
|
+
mode: mode || 'content',
|
|
828
|
+
fileName
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
this.logger.info('File attachment uploaded', {
|
|
832
|
+
agentId,
|
|
833
|
+
fileId: result.fileId,
|
|
834
|
+
fileName: result.fileName
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
res.json({
|
|
838
|
+
success: true,
|
|
839
|
+
attachment: result
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
} catch (error) {
|
|
843
|
+
this.logger.error('Failed to upload file attachment', {
|
|
844
|
+
agentId: req.params.agentId,
|
|
845
|
+
error: error.message
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
res.status(500).json({
|
|
849
|
+
success: false,
|
|
850
|
+
error: error.message
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// Get all attachments for an agent
|
|
856
|
+
this.app.get('/api/agents/:agentId/attachments', async (req, res) => {
|
|
857
|
+
try {
|
|
858
|
+
const { agentId } = req.params;
|
|
859
|
+
const { mode, active } = req.query;
|
|
860
|
+
|
|
861
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
862
|
+
return res.status(500).json({
|
|
863
|
+
success: false,
|
|
864
|
+
error: 'File attachment service not available'
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const filters = {};
|
|
869
|
+
if (mode) filters.mode = mode;
|
|
870
|
+
if (active !== undefined) filters.active = active === 'true';
|
|
871
|
+
|
|
872
|
+
const attachments = await this.orchestrator.fileAttachmentService.getAttachments(agentId, filters);
|
|
873
|
+
|
|
874
|
+
res.json({
|
|
875
|
+
success: true,
|
|
876
|
+
attachments,
|
|
877
|
+
total: attachments.length
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
} catch (error) {
|
|
881
|
+
this.logger.error('Failed to get attachments', {
|
|
882
|
+
agentId: req.params.agentId,
|
|
883
|
+
error: error.message
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
res.status(500).json({
|
|
887
|
+
success: false,
|
|
888
|
+
error: error.message
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// Get single attachment by ID
|
|
894
|
+
this.app.get('/api/attachments/:fileId', async (req, res) => {
|
|
895
|
+
try {
|
|
896
|
+
const { fileId } = req.params;
|
|
897
|
+
|
|
898
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
899
|
+
return res.status(500).json({
|
|
900
|
+
success: false,
|
|
901
|
+
error: 'File attachment service not available'
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const attachment = await this.orchestrator.fileAttachmentService.getAttachment(fileId);
|
|
906
|
+
|
|
907
|
+
if (!attachment) {
|
|
908
|
+
return res.status(404).json({
|
|
909
|
+
success: false,
|
|
910
|
+
error: 'Attachment not found'
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
res.json({
|
|
915
|
+
success: true,
|
|
916
|
+
attachment
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
} catch (error) {
|
|
920
|
+
this.logger.error('Failed to get attachment', {
|
|
921
|
+
fileId: req.params.fileId,
|
|
922
|
+
error: error.message
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
res.status(500).json({
|
|
926
|
+
success: false,
|
|
927
|
+
error: error.message
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Toggle attachment active status
|
|
933
|
+
this.app.patch('/api/attachments/:fileId/toggle', async (req, res) => {
|
|
934
|
+
try {
|
|
935
|
+
const { fileId } = req.params;
|
|
936
|
+
|
|
937
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
938
|
+
return res.status(500).json({
|
|
939
|
+
success: false,
|
|
940
|
+
error: 'File attachment service not available'
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const result = await this.orchestrator.fileAttachmentService.toggleActive(fileId);
|
|
945
|
+
|
|
946
|
+
this.logger.info('Attachment active status toggled', {
|
|
947
|
+
fileId,
|
|
948
|
+
active: result.active
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
res.json({
|
|
952
|
+
success: true,
|
|
953
|
+
attachment: result
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
} catch (error) {
|
|
957
|
+
this.logger.error('Failed to toggle attachment', {
|
|
958
|
+
fileId: req.params.fileId,
|
|
959
|
+
error: error.message
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
res.status(500).json({
|
|
963
|
+
success: false,
|
|
964
|
+
error: error.message
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// Update attachment metadata
|
|
970
|
+
this.app.patch('/api/attachments/:fileId', async (req, res) => {
|
|
971
|
+
try {
|
|
972
|
+
const { fileId } = req.params;
|
|
973
|
+
const { mode, active } = req.body;
|
|
974
|
+
|
|
975
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
976
|
+
return res.status(500).json({
|
|
977
|
+
success: false,
|
|
978
|
+
error: 'File attachment service not available'
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const updates = {};
|
|
983
|
+
if (mode !== undefined) updates.mode = mode;
|
|
984
|
+
if (active !== undefined) updates.active = active;
|
|
985
|
+
|
|
986
|
+
const result = await this.orchestrator.fileAttachmentService.updateAttachment(fileId, updates);
|
|
987
|
+
|
|
988
|
+
this.logger.info('Attachment updated', {
|
|
989
|
+
fileId,
|
|
990
|
+
updates
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
res.json({
|
|
994
|
+
success: true,
|
|
995
|
+
attachment: result
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
} catch (error) {
|
|
999
|
+
this.logger.error('Failed to update attachment', {
|
|
1000
|
+
fileId: req.params.fileId,
|
|
1001
|
+
error: error.message
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
res.status(500).json({
|
|
1005
|
+
success: false,
|
|
1006
|
+
error: error.message
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// Delete attachment
|
|
1012
|
+
this.app.delete('/api/attachments/:fileId', async (req, res) => {
|
|
1013
|
+
try {
|
|
1014
|
+
const { fileId } = req.params;
|
|
1015
|
+
const { agentId } = req.query;
|
|
1016
|
+
|
|
1017
|
+
if (!agentId) {
|
|
1018
|
+
return res.status(400).json({
|
|
1019
|
+
success: false,
|
|
1020
|
+
error: 'agentId query parameter is required'
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
1025
|
+
return res.status(500).json({
|
|
1026
|
+
success: false,
|
|
1027
|
+
error: 'File attachment service not available'
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const result = await this.orchestrator.fileAttachmentService.deleteAttachment(fileId, agentId);
|
|
1032
|
+
|
|
1033
|
+
this.logger.info('Attachment deleted', {
|
|
1034
|
+
fileId,
|
|
1035
|
+
agentId,
|
|
1036
|
+
physicallyDeleted: result.physicallyDeleted
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
res.json({
|
|
1040
|
+
success: true,
|
|
1041
|
+
message: result.physicallyDeleted ? 'Attachment deleted' : 'Reference removed',
|
|
1042
|
+
physicallyDeleted: result.physicallyDeleted
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
this.logger.error('Failed to delete attachment', {
|
|
1047
|
+
fileId: req.params.fileId,
|
|
1048
|
+
error: error.message
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
res.status(500).json({
|
|
1052
|
+
success: false,
|
|
1053
|
+
error: error.message
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// Import attachment from another agent
|
|
1059
|
+
this.app.post('/api/attachments/:fileId/import', async (req, res) => {
|
|
1060
|
+
try {
|
|
1061
|
+
const { fileId } = req.params;
|
|
1062
|
+
const { targetAgentId } = req.body;
|
|
1063
|
+
|
|
1064
|
+
if (!targetAgentId) {
|
|
1065
|
+
return res.status(400).json({
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: 'targetAgentId is required'
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
1072
|
+
return res.status(500).json({
|
|
1073
|
+
success: false,
|
|
1074
|
+
error: 'File attachment service not available'
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const result = await this.orchestrator.fileAttachmentService.importFromAgent(fileId, targetAgentId);
|
|
1079
|
+
|
|
1080
|
+
this.logger.info('Attachment imported', {
|
|
1081
|
+
fileId,
|
|
1082
|
+
targetAgentId
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
res.json({
|
|
1086
|
+
success: true,
|
|
1087
|
+
attachment: result
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
this.logger.error('Failed to import attachment', {
|
|
1092
|
+
fileId: req.params.fileId,
|
|
1093
|
+
error: error.message
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
res.status(500).json({
|
|
1097
|
+
success: false,
|
|
1098
|
+
error: error.message
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Get attachment preview
|
|
1104
|
+
this.app.get('/api/attachments/:fileId/preview', async (req, res) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const { fileId } = req.params;
|
|
1107
|
+
|
|
1108
|
+
if (!this.orchestrator?.fileAttachmentService) {
|
|
1109
|
+
return res.status(500).json({
|
|
1110
|
+
success: false,
|
|
1111
|
+
error: 'File attachment service not available'
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const preview = await this.orchestrator.fileAttachmentService.getAttachmentPreview(fileId);
|
|
1116
|
+
|
|
1117
|
+
if (!preview) {
|
|
1118
|
+
return res.status(404).json({
|
|
1119
|
+
success: false,
|
|
1120
|
+
error: 'Attachment not found'
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
res.json({
|
|
1125
|
+
success: true,
|
|
1126
|
+
preview
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
this.logger.error('Failed to get attachment preview', {
|
|
1131
|
+
fileId: req.params.fileId,
|
|
1132
|
+
error: error.message
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
res.status(500).json({
|
|
1136
|
+
success: false,
|
|
1137
|
+
error: error.message
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Image serving endpoint for generated images
|
|
1143
|
+
this.app.get('/api/images/:sessionId/:filename', async (req, res) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const { sessionId, filename } = req.params;
|
|
1146
|
+
|
|
1147
|
+
// Security validation: Check if sessionId exists
|
|
1148
|
+
const session = this.sessions.get(sessionId);
|
|
1149
|
+
if (!session) {
|
|
1150
|
+
return res.status(HTTP_STATUS.FORBIDDEN).json({
|
|
1151
|
+
success: false,
|
|
1152
|
+
error: 'Invalid session'
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Security validation: Check filename for path traversal attempts
|
|
1157
|
+
const normalizedFilename = path.basename(filename);
|
|
1158
|
+
if (normalizedFilename !== filename || filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
1159
|
+
return res.status(HTTP_STATUS.BAD_REQUEST).json({
|
|
1160
|
+
success: false,
|
|
1161
|
+
error: 'Invalid filename'
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Try to locate the image in multiple possible locations
|
|
1166
|
+
let imagePath = null;
|
|
1167
|
+
const searchPaths = [
|
|
1168
|
+
// 1. Session's project directory images folder
|
|
1169
|
+
path.join(session.projectDir || process.cwd(), 'images', normalizedFilename),
|
|
1170
|
+
// 2. Temp directory for this session
|
|
1171
|
+
path.join('/tmp/loxia-images', sessionId, normalizedFilename),
|
|
1172
|
+
// 3. General temp images directory
|
|
1173
|
+
path.join('/tmp/loxia-images', normalizedFilename)
|
|
1174
|
+
];
|
|
1175
|
+
|
|
1176
|
+
// 4. Search in agent working directories for this session
|
|
1177
|
+
// Get all agents and check their workingDirectory
|
|
1178
|
+
if (this.orchestrator?.agentPool) {
|
|
1179
|
+
try {
|
|
1180
|
+
const agents = await this.orchestrator.agentPool.getAllAgents();
|
|
1181
|
+
for (const agent of agents) {
|
|
1182
|
+
if (agent.directoryAccess?.workingDirectory) {
|
|
1183
|
+
searchPaths.push(
|
|
1184
|
+
path.join(agent.directoryAccess.workingDirectory, 'images', normalizedFilename)
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
this.logger.warn('Failed to get agent working directories for image search', {
|
|
1190
|
+
error: error.message
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Find the first existing file
|
|
1196
|
+
for (const searchPath of searchPaths) {
|
|
1197
|
+
try {
|
|
1198
|
+
const stats = await fs.stat(searchPath);
|
|
1199
|
+
if (stats.isFile()) {
|
|
1200
|
+
imagePath = searchPath;
|
|
1201
|
+
this.logger.info('Image found', {
|
|
1202
|
+
filename: normalizedFilename,
|
|
1203
|
+
path: imagePath,
|
|
1204
|
+
sessionId
|
|
1205
|
+
});
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
} catch (error) {
|
|
1209
|
+
// File doesn't exist at this path, try next one
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (!imagePath) {
|
|
1215
|
+
this.logger.warn('Image not found in any search path', {
|
|
1216
|
+
filename: normalizedFilename,
|
|
1217
|
+
sessionId,
|
|
1218
|
+
searchPaths
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
return res.status(HTTP_STATUS.NOT_FOUND).json({
|
|
1222
|
+
success: false,
|
|
1223
|
+
error: 'Image not found'
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Serve the image file
|
|
1228
|
+
res.sendFile(imagePath, (err) => {
|
|
1229
|
+
if (err) {
|
|
1230
|
+
this.logger.error('Failed to send image file', {
|
|
1231
|
+
error: err.message,
|
|
1232
|
+
imagePath,
|
|
1233
|
+
filename: normalizedFilename
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
if (!res.headersSent) {
|
|
1237
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
1238
|
+
success: false,
|
|
1239
|
+
error: 'Failed to serve image'
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
} else {
|
|
1243
|
+
this.logger.info('Image served successfully', {
|
|
1244
|
+
filename: normalizedFilename,
|
|
1245
|
+
path: imagePath,
|
|
1246
|
+
sessionId
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
this.logger.error('Image serving error', {
|
|
1253
|
+
error: error.message,
|
|
1254
|
+
sessionId: req.params.sessionId,
|
|
1255
|
+
filename: req.params.filename
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({
|
|
1259
|
+
success: false,
|
|
1260
|
+
error: error.message
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// Agent mode control endpoints
|
|
1266
|
+
this.app.post('/api/agents/:agentId/mode', async (req, res) => {
|
|
1267
|
+
try {
|
|
1268
|
+
const { agentId } = req.params;
|
|
1269
|
+
const { mode, lockMode = false, sessionId: bodySessionId } = req.body;
|
|
1270
|
+
|
|
1271
|
+
// Validate mode
|
|
1272
|
+
if (!Object.values(AGENT_MODES).includes(mode)) {
|
|
1273
|
+
return res.status(400).json({
|
|
1274
|
+
success: false,
|
|
1275
|
+
error: `Invalid mode. Must be one of: ${Object.values(AGENT_MODES).join(', ')}`
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// CRITICAL FIX: Use session ID from body first, then req.sessionID
|
|
1280
|
+
const sessionId = bodySessionId || req.sessionID;
|
|
1281
|
+
|
|
1282
|
+
if (!sessionId) {
|
|
1283
|
+
this.logger.warn('Agent mode update requested without session ID', {
|
|
1284
|
+
agentId,
|
|
1285
|
+
hasBodySessionId: !!bodySessionId,
|
|
1286
|
+
hasReqSessionId: !!req.sessionID
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Update agent mode
|
|
1291
|
+
const request = {
|
|
1292
|
+
interface: INTERFACE_TYPES.WEB,
|
|
1293
|
+
sessionId: sessionId,
|
|
1294
|
+
action: 'update_agent',
|
|
1295
|
+
payload: {
|
|
1296
|
+
agentId,
|
|
1297
|
+
updates: {
|
|
1298
|
+
mode: mode // lockMode is no longer used, only CHAT and AGENT modes
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const response = await this.orchestrator.processRequest(request);
|
|
1304
|
+
|
|
1305
|
+
if (response.success) {
|
|
1306
|
+
// Extract agent from response.data (orchestrator wraps result in data property)
|
|
1307
|
+
const updatedAgent = response.data;
|
|
1308
|
+
|
|
1309
|
+
this.logger.info(`Agent mode updated: ${agentId}`, {
|
|
1310
|
+
newMode: mode,
|
|
1311
|
+
lockMode,
|
|
1312
|
+
finalMode: updatedAgent?.mode
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
// Broadcast mode change via WebSocket
|
|
1316
|
+
this.broadcastToSession(request.sessionId, {
|
|
1317
|
+
type: 'agent_mode_changed',
|
|
1318
|
+
agentId,
|
|
1319
|
+
mode: updatedAgent?.mode,
|
|
1320
|
+
modeState: updatedAgent?.modeState
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
res.json({
|
|
1324
|
+
success: true,
|
|
1325
|
+
agent: updatedAgent,
|
|
1326
|
+
message: `Agent mode switched to ${updatedAgent?.mode}`
|
|
1327
|
+
});
|
|
1328
|
+
} else {
|
|
1329
|
+
res.status(400).json({
|
|
1330
|
+
success: false,
|
|
1331
|
+
error: response.error || 'Failed to update agent mode'
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
this.logger.error('Failed to update agent mode', {
|
|
1337
|
+
agentId: req.params.agentId,
|
|
1338
|
+
error: error.message
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
res.status(500).json({
|
|
1342
|
+
success: false,
|
|
1343
|
+
error: error.message
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// Stop autonomous execution
|
|
1349
|
+
this.app.post('/api/agents/:agentId/stop', async (req, res) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const { agentId } = req.params;
|
|
1352
|
+
|
|
1353
|
+
// Get message processor from orchestrator
|
|
1354
|
+
const messageProcessor = this.orchestrator.messageProcessor;
|
|
1355
|
+
if (!messageProcessor) {
|
|
1356
|
+
return res.status(500).json({
|
|
1357
|
+
success: false,
|
|
1358
|
+
error: 'Message processor not available'
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const result = await messageProcessor.stopAutonomousExecution(agentId);
|
|
1363
|
+
|
|
1364
|
+
this.logger.info(`Autonomous execution stop requested: ${agentId}`);
|
|
1365
|
+
|
|
1366
|
+
// Broadcast stop event via WebSocket with updated agent state
|
|
1367
|
+
// TODO: Get session ID from request body or agent context
|
|
1368
|
+
const broadcastSessionId = req.sessionID || result.agent?.sessionId;
|
|
1369
|
+
if (broadcastSessionId) {
|
|
1370
|
+
this.broadcastToSession(broadcastSessionId, {
|
|
1371
|
+
type: 'agent_mode_changed',
|
|
1372
|
+
agentId,
|
|
1373
|
+
mode: result.agent?.mode,
|
|
1374
|
+
modeState: result.agent?.modeState
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
res.json(result);
|
|
1379
|
+
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
this.logger.error('Failed to stop autonomous execution', {
|
|
1382
|
+
agentId: req.params.agentId,
|
|
1383
|
+
error: error.message
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
res.status(500).json({
|
|
1387
|
+
success: false,
|
|
1388
|
+
error: error.message
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// Get agent mode status
|
|
1394
|
+
this.app.get('/api/agents/:agentId/mode', async (req, res) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const { agentId } = req.params;
|
|
1397
|
+
|
|
1398
|
+
// TODO: Get session ID from query params or headers
|
|
1399
|
+
const request = {
|
|
1400
|
+
interface: INTERFACE_TYPES.WEB,
|
|
1401
|
+
sessionId: req.sessionID,
|
|
1402
|
+
action: 'get_agent_status',
|
|
1403
|
+
payload: { agentId }
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
const response = await this.orchestrator.processRequest(request);
|
|
1407
|
+
|
|
1408
|
+
if (response.success && response.agent) {
|
|
1409
|
+
res.json({
|
|
1410
|
+
success: true,
|
|
1411
|
+
mode: response.agent.mode,
|
|
1412
|
+
modeState: response.agent.modeState,
|
|
1413
|
+
currentTask: response.agent.currentTask,
|
|
1414
|
+
iterationCount: response.agent.iterationCount,
|
|
1415
|
+
taskStartTime: response.agent.taskStartTime,
|
|
1416
|
+
stopRequested: response.agent.stopRequested
|
|
1417
|
+
});
|
|
1418
|
+
} else {
|
|
1419
|
+
res.status(404).json({
|
|
1420
|
+
success: false,
|
|
1421
|
+
error: 'Agent not found'
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
this.logger.error('Failed to get agent mode status', {
|
|
1427
|
+
agentId: req.params.agentId,
|
|
1428
|
+
error: error.message
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
res.status(500).json({
|
|
1432
|
+
success: false,
|
|
1433
|
+
error: error.message
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
// Agent Import/Resume Endpoints
|
|
1439
|
+
|
|
1440
|
+
// Get all available agents (active + archived)
|
|
1441
|
+
this.app.get('/api/agents/available', async (req, res) => {
|
|
1442
|
+
try {
|
|
1443
|
+
const projectDir = this.orchestrator.config.project?.directory || process.cwd();
|
|
1444
|
+
const agents = await this.orchestrator.stateManager.getAllAvailableAgents(
|
|
1445
|
+
projectDir,
|
|
1446
|
+
this.orchestrator.agentPool
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
res.json({
|
|
1450
|
+
success: true,
|
|
1451
|
+
agents: agents,
|
|
1452
|
+
total: agents.length,
|
|
1453
|
+
active: agents.filter(a => a.isLoaded).length,
|
|
1454
|
+
archived: agents.filter(a => !a.isLoaded).length
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
this.logger.error('Failed to get available agents', {
|
|
1458
|
+
error: error.message,
|
|
1459
|
+
stack: error.stack
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
res.status(500).json({
|
|
1463
|
+
success: false,
|
|
1464
|
+
error: error.message
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Get agent metadata for preview
|
|
1470
|
+
this.app.get('/api/agents/:agentId/metadata', async (req, res) => {
|
|
1471
|
+
try {
|
|
1472
|
+
const { agentId } = req.params;
|
|
1473
|
+
const projectDir = this.orchestrator.config.project?.directory || process.cwd();
|
|
1474
|
+
|
|
1475
|
+
const metadata = await this.orchestrator.stateManager.getAgentMetadata(
|
|
1476
|
+
agentId,
|
|
1477
|
+
projectDir
|
|
1478
|
+
);
|
|
1479
|
+
|
|
1480
|
+
res.json({
|
|
1481
|
+
success: true,
|
|
1482
|
+
metadata
|
|
1483
|
+
});
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
this.logger.error('Failed to get agent metadata', {
|
|
1486
|
+
agentId: req.params.agentId,
|
|
1487
|
+
error: error.message
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
res.status(404).json({
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: error.message
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
// Import archived agent
|
|
1498
|
+
this.app.post('/api/agents/import', async (req, res) => {
|
|
1499
|
+
try {
|
|
1500
|
+
const { agentId } = req.body;
|
|
1501
|
+
|
|
1502
|
+
if (!agentId) {
|
|
1503
|
+
return res.status(400).json({
|
|
1504
|
+
success: false,
|
|
1505
|
+
error: 'agentId is required in request body'
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
const projectDir = this.orchestrator.config.project?.directory || process.cwd();
|
|
1510
|
+
|
|
1511
|
+
// Import the agent
|
|
1512
|
+
const agent = await this.orchestrator.stateManager.importArchivedAgent(
|
|
1513
|
+
agentId,
|
|
1514
|
+
projectDir,
|
|
1515
|
+
this.orchestrator.agentPool
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
// Broadcast agent added event via WebSocket
|
|
1519
|
+
if (this.wsManager) {
|
|
1520
|
+
this.wsManager.broadcast({
|
|
1521
|
+
type: 'agent-imported',
|
|
1522
|
+
agent: {
|
|
1523
|
+
id: agent.id,
|
|
1524
|
+
name: agent.name,
|
|
1525
|
+
status: agent.status,
|
|
1526
|
+
capabilities: agent.capabilities,
|
|
1527
|
+
model: agent.currentModel || agent.preferredModel
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
this.logger.info('Agent imported successfully', {
|
|
1533
|
+
agentId: agent.id,
|
|
1534
|
+
name: agent.name
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
res.json({
|
|
1538
|
+
success: true,
|
|
1539
|
+
agent: {
|
|
1540
|
+
id: agent.id,
|
|
1541
|
+
name: agent.name,
|
|
1542
|
+
status: agent.status,
|
|
1543
|
+
model: agent.currentModel || agent.preferredModel,
|
|
1544
|
+
capabilities: agent.capabilities,
|
|
1545
|
+
lastActivity: agent.lastActivity
|
|
1546
|
+
},
|
|
1547
|
+
message: `Agent ${agent.name} imported successfully`
|
|
1548
|
+
});
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
this.logger.error('Failed to import agent', {
|
|
1551
|
+
agentId: req.body.agentId,
|
|
1552
|
+
error: error.message,
|
|
1553
|
+
stack: error.stack
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// Determine appropriate status code
|
|
1557
|
+
const statusCode = error.message.includes('already active') ? 409 :
|
|
1558
|
+
error.message.includes('not found') ? 404 :
|
|
1559
|
+
error.message.includes('Invalid') ? 400 : 500;
|
|
1560
|
+
|
|
1561
|
+
res.status(statusCode).json({
|
|
1562
|
+
success: false,
|
|
1563
|
+
error: error.message
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
// Serve React app for all other routes
|
|
1569
|
+
this.app.get('*', (req, res) => {
|
|
1570
|
+
const indexPath = path.join(__dirname, '../../web-ui/build/index.html');
|
|
1571
|
+
res.sendFile(indexPath, (err) => {
|
|
1572
|
+
if (err) {
|
|
1573
|
+
res.status(HTTP_STATUS.NOT_FOUND).send('Web UI not built. Run: npm run build:ui');
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Setup WebSocket server
|
|
1581
|
+
* @private
|
|
1582
|
+
*/
|
|
1583
|
+
setupWebSocket() {
|
|
1584
|
+
this.logger.info('Setting up WebSocket server', {
|
|
1585
|
+
port: this.port,
|
|
1586
|
+
host: this.host,
|
|
1587
|
+
wsServerExists: !!this.wss,
|
|
1588
|
+
httpServerExists: !!this.server
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
// Add error handler for WebSocket server
|
|
1592
|
+
this.wss.on('error', (error) => {
|
|
1593
|
+
this.logger.error('WebSocket server error:', {
|
|
1594
|
+
error: error.message,
|
|
1595
|
+
stack: error.stack,
|
|
1596
|
+
port: this.port
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// Log when WebSocket server is ready
|
|
1601
|
+
this.wss.on('listening', () => {
|
|
1602
|
+
this.logger.info('WebSocket server is now listening', {
|
|
1603
|
+
port: this.port,
|
|
1604
|
+
host: this.host
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
this.wss.on('connection', (ws, req) => {
|
|
1609
|
+
const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1610
|
+
|
|
1611
|
+
this.logger.info('WebSocket connection established', {
|
|
1612
|
+
connectionId,
|
|
1613
|
+
ip: req.socket.remoteAddress,
|
|
1614
|
+
origin: req.headers.origin,
|
|
1615
|
+
host: req.headers.host,
|
|
1616
|
+
userAgent: req.headers['user-agent'],
|
|
1617
|
+
url: req.url
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// Store connection
|
|
1621
|
+
const connection = {
|
|
1622
|
+
id: connectionId,
|
|
1623
|
+
ws,
|
|
1624
|
+
sessionId: null,
|
|
1625
|
+
connectedAt: new Date().toISOString(),
|
|
1626
|
+
lastActivity: new Date().toISOString()
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
this.connections.set(connectionId, connection);
|
|
1630
|
+
|
|
1631
|
+
// Handle messages
|
|
1632
|
+
ws.on('message', async (data) => {
|
|
1633
|
+
try {
|
|
1634
|
+
const message = JSON.parse(data.toString());
|
|
1635
|
+
await this.handleWebSocketMessage(connectionId, message);
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
this.logger.error('WebSocket message error', {
|
|
1638
|
+
connectionId,
|
|
1639
|
+
error: error.message
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
ws.send(JSON.stringify({
|
|
1643
|
+
type: 'error',
|
|
1644
|
+
error: error.message
|
|
1645
|
+
}));
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
// Handle disconnect
|
|
1650
|
+
ws.on('close', () => {
|
|
1651
|
+
this.logger.info('WebSocket connection closed', { connectionId });
|
|
1652
|
+
this.connections.delete(connectionId);
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
// Send welcome message
|
|
1656
|
+
ws.send(JSON.stringify({
|
|
1657
|
+
type: 'connected',
|
|
1658
|
+
connectionId,
|
|
1659
|
+
timestamp: new Date().toISOString()
|
|
1660
|
+
}));
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Handle WebSocket message
|
|
1666
|
+
* @private
|
|
1667
|
+
*/
|
|
1668
|
+
async handleWebSocketMessage(connectionId, message) {
|
|
1669
|
+
const connection = this.connections.get(connectionId);
|
|
1670
|
+
if (!connection) return;
|
|
1671
|
+
|
|
1672
|
+
connection.lastActivity = new Date().toISOString();
|
|
1673
|
+
|
|
1674
|
+
switch (message.type) {
|
|
1675
|
+
case 'join_session':
|
|
1676
|
+
const sessionId = message.sessionId;
|
|
1677
|
+
connection.sessionId = sessionId;
|
|
1678
|
+
|
|
1679
|
+
this.logger.info('WebSocket joined session', {
|
|
1680
|
+
connectionId,
|
|
1681
|
+
sessionId,
|
|
1682
|
+
totalConnectionsForSession: Array.from(this.connections.values()).filter(c => c.sessionId === sessionId).length
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
connection.ws.send(JSON.stringify({
|
|
1686
|
+
type: 'session_joined',
|
|
1687
|
+
sessionId: sessionId
|
|
1688
|
+
}));
|
|
1689
|
+
break;
|
|
1690
|
+
|
|
1691
|
+
case 'ping':
|
|
1692
|
+
connection.ws.send(JSON.stringify({
|
|
1693
|
+
type: 'pong',
|
|
1694
|
+
timestamp: new Date().toISOString()
|
|
1695
|
+
}));
|
|
1696
|
+
break;
|
|
1697
|
+
|
|
1698
|
+
case 'orchestrator_request':
|
|
1699
|
+
// Handle real-time orchestrator requests
|
|
1700
|
+
try {
|
|
1701
|
+
const request = {
|
|
1702
|
+
interface: INTERFACE_TYPES.WEB,
|
|
1703
|
+
sessionId: connection.sessionId,
|
|
1704
|
+
action: message.action,
|
|
1705
|
+
payload: message.payload,
|
|
1706
|
+
projectDir: message.projectDir || process.cwd()
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
const response = await this.orchestrator.processRequest(request);
|
|
1710
|
+
|
|
1711
|
+
connection.ws.send(JSON.stringify({
|
|
1712
|
+
type: 'orchestrator_response',
|
|
1713
|
+
requestId: message.requestId,
|
|
1714
|
+
response
|
|
1715
|
+
}));
|
|
1716
|
+
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
connection.ws.send(JSON.stringify({
|
|
1719
|
+
type: 'error',
|
|
1720
|
+
requestId: message.requestId,
|
|
1721
|
+
error: error.message
|
|
1722
|
+
}));
|
|
1723
|
+
}
|
|
1724
|
+
break;
|
|
1725
|
+
|
|
1726
|
+
default:
|
|
1727
|
+
this.logger.warn('Unknown WebSocket message type', {
|
|
1728
|
+
connectionId,
|
|
1729
|
+
type: message.type
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/**
|
|
1735
|
+
* Broadcast message to all connections in a session
|
|
1736
|
+
* @private
|
|
1737
|
+
*/
|
|
1738
|
+
broadcastToSession(sessionId, message) {
|
|
1739
|
+
const sessionConnections = Array.from(this.connections.values())
|
|
1740
|
+
.filter(conn => conn.sessionId === sessionId);
|
|
1741
|
+
|
|
1742
|
+
// If no connections found for this session, try broadcasting to all connections
|
|
1743
|
+
// This handles cases where session IDs might be mismatched
|
|
1744
|
+
let allConnections = [];
|
|
1745
|
+
if (sessionConnections.length === 0) {
|
|
1746
|
+
allConnections = Array.from(this.connections.values());
|
|
1747
|
+
|
|
1748
|
+
this.logger?.warn('š No connections for session, trying all connections:', {
|
|
1749
|
+
targetSessionId: sessionId,
|
|
1750
|
+
totalConnections: this.connections.size,
|
|
1751
|
+
allSessionIds: Array.from(this.connections.values()).map(c => c.sessionId).filter(Boolean)
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const targetConnections = sessionConnections.length > 0 ? sessionConnections : allConnections;
|
|
1756
|
+
|
|
1757
|
+
this.logger?.info('š” WebSocket broadcastToSession called:', {
|
|
1758
|
+
sessionId,
|
|
1759
|
+
messageType: message.type,
|
|
1760
|
+
agentId: message.agentId,
|
|
1761
|
+
totalConnections: this.connections.size,
|
|
1762
|
+
sessionConnections: sessionConnections.length,
|
|
1763
|
+
targetConnections: targetConnections.length,
|
|
1764
|
+
connectionIds: targetConnections.map(c => c.id),
|
|
1765
|
+
usingFallback: sessionConnections.length === 0,
|
|
1766
|
+
messagePreview: message.type === 'autonomous_update' && message.message ? {
|
|
1767
|
+
messageId: message.message.id,
|
|
1768
|
+
messageRole: message.message.role,
|
|
1769
|
+
contentLength: message.message.content?.length,
|
|
1770
|
+
hasToolResults: !!message.message.toolResults
|
|
1771
|
+
} : undefined
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
for (const connection of targetConnections) {
|
|
1775
|
+
try {
|
|
1776
|
+
const fullMessage = {
|
|
1777
|
+
...message,
|
|
1778
|
+
timestamp: new Date().toISOString()
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
connection.ws.send(JSON.stringify(fullMessage));
|
|
1782
|
+
|
|
1783
|
+
this.logger?.info('ā
WebSocket message sent to connection:', {
|
|
1784
|
+
connectionId: connection.id,
|
|
1785
|
+
messageType: message.type,
|
|
1786
|
+
agentId: message.agentId
|
|
1787
|
+
});
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
this.logger.warn('ā Failed to send WebSocket message', {
|
|
1790
|
+
connectionId: connection.id,
|
|
1791
|
+
sessionId,
|
|
1792
|
+
messageType: message.type,
|
|
1793
|
+
error: error.message
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* Start the HTTP server
|
|
1801
|
+
* @private
|
|
1802
|
+
*/
|
|
1803
|
+
async startServer() {
|
|
1804
|
+
return new Promise((resolve, reject) => {
|
|
1805
|
+
this.server.listen(this.port, this.host, (error) => {
|
|
1806
|
+
if (error) {
|
|
1807
|
+
this.logger.error('Failed to start HTTP server', {
|
|
1808
|
+
error: error.message,
|
|
1809
|
+
port: this.port,
|
|
1810
|
+
host: this.host
|
|
1811
|
+
});
|
|
1812
|
+
reject(error);
|
|
1813
|
+
} else {
|
|
1814
|
+
this.isRunning = true;
|
|
1815
|
+
|
|
1816
|
+
// Verify WebSocket server status
|
|
1817
|
+
this.logger.info('HTTP server started successfully', {
|
|
1818
|
+
port: this.port,
|
|
1819
|
+
host: this.host,
|
|
1820
|
+
httpUrl: `http://${this.host}:${this.port}`,
|
|
1821
|
+
wsUrl: `ws://${this.host}:${this.port}`,
|
|
1822
|
+
wsServerAttached: !!this.wss,
|
|
1823
|
+
wsConnections: this.connections.size
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
// Test WebSocket server availability
|
|
1827
|
+
setTimeout(async () => {
|
|
1828
|
+
await this.testWebSocketServer();
|
|
1829
|
+
}, 1000);
|
|
1830
|
+
|
|
1831
|
+
resolve();
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// Add server error handler
|
|
1836
|
+
this.server.on('error', (error) => {
|
|
1837
|
+
this.logger.error('HTTP server error:', {
|
|
1838
|
+
error: error.message,
|
|
1839
|
+
stack: error.stack,
|
|
1840
|
+
port: this.port
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/**
|
|
1847
|
+
* Test WebSocket server availability
|
|
1848
|
+
* @private
|
|
1849
|
+
*/
|
|
1850
|
+
async testWebSocketServer() {
|
|
1851
|
+
try {
|
|
1852
|
+
const { default: WebSocket } = await import('ws');
|
|
1853
|
+
const testWs = new WebSocket(`ws://localhost:${this.port}`);
|
|
1854
|
+
|
|
1855
|
+
testWs.on('open', () => {
|
|
1856
|
+
this.logger.info('ā
WebSocket server test: SUCCESSFUL', {
|
|
1857
|
+
port: this.port,
|
|
1858
|
+
url: `ws://localhost:${this.port}`
|
|
1859
|
+
});
|
|
1860
|
+
testWs.close();
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
testWs.on('error', (error) => {
|
|
1864
|
+
this.logger.error('ā WebSocket server test: FAILED', {
|
|
1865
|
+
port: this.port,
|
|
1866
|
+
url: `ws://localhost:${this.port}`,
|
|
1867
|
+
error: error.message,
|
|
1868
|
+
code: error.code
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
testWs.on('close', (code, reason) => {
|
|
1873
|
+
if (code === 1000) {
|
|
1874
|
+
this.logger.info('WebSocket test connection closed cleanly');
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
// Timeout the test
|
|
1879
|
+
setTimeout(() => {
|
|
1880
|
+
if (testWs.readyState === WebSocket.CONNECTING) {
|
|
1881
|
+
this.logger.error('ā WebSocket server test: TIMEOUT', {
|
|
1882
|
+
port: this.port,
|
|
1883
|
+
url: `ws://localhost:${this.port}`,
|
|
1884
|
+
readyState: testWs.readyState
|
|
1885
|
+
});
|
|
1886
|
+
testWs.terminate();
|
|
1887
|
+
}
|
|
1888
|
+
}, 5000);
|
|
1889
|
+
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
this.logger.error('ā WebSocket server test: EXCEPTION', {
|
|
1892
|
+
error: error.message,
|
|
1893
|
+
stack: error.stack
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Get default models when API is unavailable
|
|
1900
|
+
* @returns {Array} Default model list
|
|
1901
|
+
* @private
|
|
1902
|
+
*/
|
|
1903
|
+
getDefaultModels() {
|
|
1904
|
+
return [
|
|
1905
|
+
{
|
|
1906
|
+
name: 'anthropic-sonnet',
|
|
1907
|
+
category: 'anthropic',
|
|
1908
|
+
type: 'chat',
|
|
1909
|
+
maxTokens: 10000,
|
|
1910
|
+
supportsVision: true,
|
|
1911
|
+
supportsSystem: true,
|
|
1912
|
+
pricing: {
|
|
1913
|
+
input: 0.003,
|
|
1914
|
+
output: 0.015,
|
|
1915
|
+
unit: '1K tokens'
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
name: 'anthropic-opus',
|
|
1920
|
+
category: 'anthropic',
|
|
1921
|
+
type: 'chat',
|
|
1922
|
+
maxTokens: 10000,
|
|
1923
|
+
supportsVision: true,
|
|
1924
|
+
supportsSystem: true,
|
|
1925
|
+
pricing: {
|
|
1926
|
+
input: 0.015,
|
|
1927
|
+
output: 0.075,
|
|
1928
|
+
unit: '1K tokens'
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
name: 'anthropic-haiku',
|
|
1933
|
+
category: 'anthropic',
|
|
1934
|
+
type: 'chat',
|
|
1935
|
+
maxTokens: 10000,
|
|
1936
|
+
supportsVision: false,
|
|
1937
|
+
supportsSystem: true,
|
|
1938
|
+
pricing: {
|
|
1939
|
+
input: 0.0025,
|
|
1940
|
+
output: 0.0125,
|
|
1941
|
+
unit: '1K tokens'
|
|
1942
|
+
}
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
name: 'gpt-4',
|
|
1946
|
+
category: 'openai',
|
|
1947
|
+
type: 'chat',
|
|
1948
|
+
maxTokens: 8000,
|
|
1949
|
+
supportsVision: true,
|
|
1950
|
+
supportsSystem: true,
|
|
1951
|
+
pricing: {
|
|
1952
|
+
input: 0.03,
|
|
1953
|
+
output: 0.06,
|
|
1954
|
+
unit: '1K tokens'
|
|
1955
|
+
}
|
|
1956
|
+
},
|
|
1957
|
+
{
|
|
1958
|
+
name: 'gpt-4-mini',
|
|
1959
|
+
category: 'openai',
|
|
1960
|
+
type: 'chat',
|
|
1961
|
+
maxTokens: 16000,
|
|
1962
|
+
supportsVision: false,
|
|
1963
|
+
supportsSystem: true,
|
|
1964
|
+
pricing: {
|
|
1965
|
+
input: 0.0015,
|
|
1966
|
+
output: 0.006,
|
|
1967
|
+
unit: '1K tokens'
|
|
1968
|
+
}
|
|
1969
|
+
},
|
|
1970
|
+
{
|
|
1971
|
+
name: 'deepseek-r1',
|
|
1972
|
+
category: 'deepseek',
|
|
1973
|
+
type: 'chat',
|
|
1974
|
+
maxTokens: 8000,
|
|
1975
|
+
supportsVision: false,
|
|
1976
|
+
supportsSystem: true,
|
|
1977
|
+
pricing: {
|
|
1978
|
+
input: 0.002,
|
|
1979
|
+
output: 0.008,
|
|
1980
|
+
unit: '1K tokens'
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
name: 'phi-4',
|
|
1985
|
+
category: 'microsoft',
|
|
1986
|
+
type: 'chat',
|
|
1987
|
+
maxTokens: 4000,
|
|
1988
|
+
supportsVision: false,
|
|
1989
|
+
supportsSystem: true,
|
|
1990
|
+
pricing: {
|
|
1991
|
+
input: 0.001,
|
|
1992
|
+
output: 0.004,
|
|
1993
|
+
unit: '1K tokens'
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
];
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* Set API key manager instance
|
|
2001
|
+
* @param {ApiKeyManager} apiKeyManager - API key manager instance
|
|
2002
|
+
*/
|
|
2003
|
+
setApiKeyManager(apiKeyManager) {
|
|
2004
|
+
this.apiKeyManager = apiKeyManager;
|
|
2005
|
+
|
|
2006
|
+
this.logger?.info('API key manager set for web server', {
|
|
2007
|
+
hasManager: !!apiKeyManager
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
/**
|
|
2012
|
+
* Extract vendor name from model name
|
|
2013
|
+
* @param {string} model - Model name
|
|
2014
|
+
* @returns {string|null} Vendor name
|
|
2015
|
+
* @private
|
|
2016
|
+
*/
|
|
2017
|
+
_getVendorFromModel(model) {
|
|
2018
|
+
if (!model) return null;
|
|
2019
|
+
|
|
2020
|
+
const modelName = model.toLowerCase();
|
|
2021
|
+
|
|
2022
|
+
if (modelName.includes('anthropic') || modelName.includes('claude')) {
|
|
2023
|
+
return 'anthropic';
|
|
2024
|
+
} else if (modelName.includes('openai') || modelName.includes('gpt')) {
|
|
2025
|
+
return 'openai';
|
|
2026
|
+
} else if (modelName.includes('deepseek')) {
|
|
2027
|
+
return 'deepseek';
|
|
2028
|
+
} else if (modelName.includes('phi')) {
|
|
2029
|
+
return 'microsoft';
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Get server status
|
|
2037
|
+
* @returns {Object} Server status
|
|
2038
|
+
*/
|
|
2039
|
+
getStatus() {
|
|
2040
|
+
return {
|
|
2041
|
+
isRunning: this.isRunning,
|
|
2042
|
+
port: this.port,
|
|
2043
|
+
host: this.host,
|
|
2044
|
+
connections: this.connections.size,
|
|
2045
|
+
sessions: this.sessions.size,
|
|
2046
|
+
url: `http://${this.host}:${this.port}`
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/**
|
|
2051
|
+
* Shutdown the web server
|
|
2052
|
+
* @returns {Promise<void>}
|
|
2053
|
+
*/
|
|
2054
|
+
async shutdown() {
|
|
2055
|
+
if (!this.isRunning) return;
|
|
2056
|
+
|
|
2057
|
+
this.logger.info('Shutting down web server...');
|
|
2058
|
+
|
|
2059
|
+
// Close all WebSocket connections
|
|
2060
|
+
for (const connection of this.connections.values()) {
|
|
2061
|
+
connection.ws.close();
|
|
2062
|
+
}
|
|
2063
|
+
this.connections.clear();
|
|
2064
|
+
|
|
2065
|
+
// Close WebSocket server
|
|
2066
|
+
this.wss.close();
|
|
2067
|
+
|
|
2068
|
+
// Close HTTP server
|
|
2069
|
+
return new Promise((resolve) => {
|
|
2070
|
+
this.server.close(() => {
|
|
2071
|
+
this.isRunning = false;
|
|
2072
|
+
this.logger.info('Web server shutdown complete');
|
|
2073
|
+
resolve();
|
|
2074
|
+
});
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
export default WebServer;
|
|
2080
|
+
|
|
2081
|
+
// Main execution block - start server if run directly
|
|
2082
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
2083
|
+
// Simple console logger for standalone mode
|
|
2084
|
+
const simpleLogger = {
|
|
2085
|
+
info: (msg, data) => console.log(`[INFO] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
|
|
2086
|
+
error: (msg, data) => console.error(`[ERROR] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
|
|
2087
|
+
warn: (msg, data) => console.warn(`[WARN] ${msg}`, data ? JSON.stringify(data, null, 2) : ''),
|
|
2088
|
+
debug: (msg, data) => console.log(`[DEBUG] ${msg}`, data ? JSON.stringify(data, null, 2) : '')
|
|
2089
|
+
};
|
|
2090
|
+
|
|
2091
|
+
// Simple orchestrator mock for standalone mode
|
|
2092
|
+
const mockOrchestrator = {
|
|
2093
|
+
processAction: async (action, data) => {
|
|
2094
|
+
simpleLogger.info('Mock orchestrator action', { action, data });
|
|
2095
|
+
|
|
2096
|
+
// Mock responses for different actions
|
|
2097
|
+
switch (action) {
|
|
2098
|
+
case ORCHESTRATOR_ACTIONS.LIST_AGENTS:
|
|
2099
|
+
return {
|
|
2100
|
+
success: true,
|
|
2101
|
+
data: []
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
case ORCHESTRATOR_ACTIONS.CREATE_AGENT:
|
|
2105
|
+
return {
|
|
2106
|
+
success: true,
|
|
2107
|
+
data: {
|
|
2108
|
+
id: `agent-${Date.now()}`,
|
|
2109
|
+
name: data.name || 'New Agent',
|
|
2110
|
+
status: 'active',
|
|
2111
|
+
model: data.model || 'anthropic-sonnet',
|
|
2112
|
+
systemPrompt: data.systemPrompt || 'You are a helpful AI assistant.'
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
case ORCHESTRATOR_ACTIONS.SEND_MESSAGE:
|
|
2117
|
+
return {
|
|
2118
|
+
success: true,
|
|
2119
|
+
data: {
|
|
2120
|
+
message: {
|
|
2121
|
+
id: `msg-${Date.now()}`,
|
|
2122
|
+
content: `Echo: ${data.message}`,
|
|
2123
|
+
timestamp: new Date().toISOString()
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
default:
|
|
2129
|
+
return {
|
|
2130
|
+
success: false,
|
|
2131
|
+
error: `Unknown action: ${action}`
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
|
|
2137
|
+
const server = new WebServer(mockOrchestrator, simpleLogger, {
|
|
2138
|
+
port: 8080,
|
|
2139
|
+
host: '0.0.0.0'
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
console.log('š Starting Loxia Web Server in standalone mode...');
|
|
2143
|
+
|
|
2144
|
+
server.startServer()
|
|
2145
|
+
.then(() => {
|
|
2146
|
+
const status = server.getStatus();
|
|
2147
|
+
console.log(`ā
Web Server running at ${status.url}`);
|
|
2148
|
+
console.log('š± Web UI available at: http://localhost:3001 (if running)');
|
|
2149
|
+
console.log('š§ API available at: http://localhost:8080/api');
|
|
2150
|
+
})
|
|
2151
|
+
.catch(error => {
|
|
2152
|
+
console.error('ā Failed to start web server:', error.message);
|
|
2153
|
+
process.exit(1);
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
// Graceful shutdown
|
|
2157
|
+
process.on('SIGINT', async () => {
|
|
2158
|
+
console.log('\nš Shutting down web server...');
|
|
2159
|
+
await server.shutdown();
|
|
2160
|
+
process.exit(0);
|
|
2161
|
+
});
|
|
2162
|
+
}
|