@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,1232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIService - Manages communication with Azure backend API, model routing, rate limiting
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Backend API communication
|
|
6
|
+
* - Model selection and routing
|
|
7
|
+
* - Rate limiting enforcement
|
|
8
|
+
* - Conversation compactization
|
|
9
|
+
* - Token usage tracking
|
|
10
|
+
* - Request/response transformation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
MODELS,
|
|
15
|
+
MODEL_PROVIDERS,
|
|
16
|
+
MODEL_ROUTING,
|
|
17
|
+
HTTP_STATUS,
|
|
18
|
+
ERROR_TYPES,
|
|
19
|
+
SYSTEM_DEFAULTS
|
|
20
|
+
} from '../utilities/constants.js';
|
|
21
|
+
|
|
22
|
+
class AIService {
|
|
23
|
+
constructor(config, logger, budgetService, errorHandler) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
this.budgetService = budgetService;
|
|
27
|
+
this.errorHandler = errorHandler;
|
|
28
|
+
|
|
29
|
+
this.baseUrl = config.backend?.baseUrl || 'https://api.loxia.ai';
|
|
30
|
+
this.timeout = config.backend?.timeout || 60000;
|
|
31
|
+
this.retryAttempts = config.backend?.retryAttempts || 3;
|
|
32
|
+
|
|
33
|
+
// Rate limiting
|
|
34
|
+
this.rateLimiters = new Map();
|
|
35
|
+
this.requestQueue = new Map();
|
|
36
|
+
|
|
37
|
+
// Circuit breaker
|
|
38
|
+
this.circuitBreaker = {
|
|
39
|
+
failures: 0,
|
|
40
|
+
lastFailureTime: null,
|
|
41
|
+
isOpen: false,
|
|
42
|
+
threshold: 5,
|
|
43
|
+
timeout: 30000 // 30 seconds
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Model specifications
|
|
47
|
+
this.modelSpecs = this._initializeModelSpecs();
|
|
48
|
+
|
|
49
|
+
// Conversation managers for multi-model support
|
|
50
|
+
this.conversationManagers = new Map();
|
|
51
|
+
|
|
52
|
+
// API Key Manager reference (will be set by LoxiaSystem)
|
|
53
|
+
this.apiKeyManager = null;
|
|
54
|
+
|
|
55
|
+
// Agent Pool reference (will be set by LoxiaSystem)
|
|
56
|
+
this.agentPool = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send message to backend API
|
|
61
|
+
* @param {string} model - Target model name
|
|
62
|
+
* @param {string|Array} messages - Message content or conversation history
|
|
63
|
+
* @param {Object} options - Additional options (agentId, systemPrompt, etc.)
|
|
64
|
+
* @returns {Promise<Object>} API response with content and metadata
|
|
65
|
+
*/
|
|
66
|
+
async sendMessage(model, messages, options = {}) {
|
|
67
|
+
const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Check circuit breaker
|
|
71
|
+
if (this._isCircuitBreakerOpen()) {
|
|
72
|
+
throw new Error('Service temporarily unavailable - circuit breaker is open');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Skip local model validation - let Azure backend handle it
|
|
76
|
+
|
|
77
|
+
// Check rate limits
|
|
78
|
+
await this._checkRateLimit(model);
|
|
79
|
+
|
|
80
|
+
// Format messages for specific model
|
|
81
|
+
const formattedMessages = this._formatMessagesForModel(messages, model, options);
|
|
82
|
+
|
|
83
|
+
// Prepare request payload
|
|
84
|
+
const payload = {
|
|
85
|
+
model, // Use original model name - let Azure backend handle it
|
|
86
|
+
messages: formattedMessages,
|
|
87
|
+
options: {
|
|
88
|
+
max_tokens: this.modelSpecs[model]?.maxTokens || 4000,
|
|
89
|
+
temperature: options.temperature || 0.7,
|
|
90
|
+
stream: options.stream || false
|
|
91
|
+
},
|
|
92
|
+
metadata: {
|
|
93
|
+
requestId,
|
|
94
|
+
agentId: options.agentId,
|
|
95
|
+
timestamp: new Date().toISOString()
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Add system prompt if provided
|
|
100
|
+
if (options.systemPrompt) {
|
|
101
|
+
payload.system = options.systemPrompt;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.logger.info(`Sending message to model: ${model}`, {
|
|
105
|
+
requestId,
|
|
106
|
+
agentId: options.agentId,
|
|
107
|
+
messageCount: Array.isArray(messages) ? messages.length : 1,
|
|
108
|
+
maxTokens: payload.max_tokens
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Make API request
|
|
112
|
+
const response = await this._makeAPIRequest('/chat/completions', payload, requestId, {
|
|
113
|
+
...options,
|
|
114
|
+
sessionId: options.sessionId, // Pass session ID for API key retrieval
|
|
115
|
+
platformProvided: options.platformProvided || false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Track usage
|
|
119
|
+
if (response.usage) {
|
|
120
|
+
await this.trackUsage(options.agentId, model, {
|
|
121
|
+
prompt_tokens: response.usage.prompt_tokens || 0,
|
|
122
|
+
completion_tokens: response.usage.completion_tokens || 0,
|
|
123
|
+
total_tokens: response.usage.total_tokens || 0
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reset circuit breaker on success
|
|
128
|
+
this._resetCircuitBreaker();
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: response.choices[0]?.message?.content || '',
|
|
132
|
+
model: response.model,
|
|
133
|
+
tokenUsage: response.usage,
|
|
134
|
+
requestId,
|
|
135
|
+
finishReason: response.choices[0]?.finish_reason
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Handle circuit breaker
|
|
140
|
+
this._recordFailure();
|
|
141
|
+
|
|
142
|
+
this.logger.error(`AI service request failed: ${error.message}`, {
|
|
143
|
+
requestId,
|
|
144
|
+
model,
|
|
145
|
+
agentId: options.agentId,
|
|
146
|
+
error: error.stack
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Handle specific error types
|
|
150
|
+
await this.handleHttpError(error, { requestId, model, agentId: options.agentId });
|
|
151
|
+
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Route model selection based on task and context
|
|
158
|
+
* @param {string} task - Task type (coding, analysis, quick-tasks, creative)
|
|
159
|
+
* @param {Object} context - Additional context for routing
|
|
160
|
+
* @returns {Promise<string>} Selected model name
|
|
161
|
+
*/
|
|
162
|
+
async routeModel(task, context = {}) {
|
|
163
|
+
try {
|
|
164
|
+
// Get available models for task
|
|
165
|
+
const availableModels = MODEL_ROUTING[task.toUpperCase()] || MODEL_ROUTING.FALLBACK;
|
|
166
|
+
|
|
167
|
+
// Check model availability and health
|
|
168
|
+
const healthyModels = [];
|
|
169
|
+
for (const model of availableModels) {
|
|
170
|
+
const isHealthy = await this._checkModelHealth(model);
|
|
171
|
+
if (isHealthy) {
|
|
172
|
+
healthyModels.push(model);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (healthyModels.length === 0) {
|
|
177
|
+
this.logger.warn(`No healthy models available for task: ${task}, using fallback`);
|
|
178
|
+
return MODEL_ROUTING.FALLBACK[0];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Select model based on context
|
|
182
|
+
let selectedModel = healthyModels[0]; // Default to first healthy model
|
|
183
|
+
|
|
184
|
+
// Prefer model based on context
|
|
185
|
+
if (context.preferredModel && healthyModels.includes(context.preferredModel)) {
|
|
186
|
+
selectedModel = context.preferredModel;
|
|
187
|
+
} else if (context.complexity === 'high' && healthyModels.includes(MODELS.ANTHROPIC_OPUS)) {
|
|
188
|
+
selectedModel = MODELS.ANTHROPIC_OPUS;
|
|
189
|
+
} else if (context.speed === 'fast' && healthyModels.includes(MODELS.ANTHROPIC_HAIKU)) {
|
|
190
|
+
selectedModel = MODELS.ANTHROPIC_HAIKU;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.logger.info(`Model routed for task: ${task}`, {
|
|
194
|
+
selectedModel,
|
|
195
|
+
availableModels: healthyModels.length,
|
|
196
|
+
context
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return selectedModel;
|
|
200
|
+
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.logger.error(`Model routing failed: ${error.message}`, { task, context });
|
|
203
|
+
return MODEL_ROUTING.FALLBACK[0];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Compactize conversation for specific model context window
|
|
209
|
+
* @param {Array} messages - Message history
|
|
210
|
+
* @param {string} targetModel - Target model name
|
|
211
|
+
* @returns {Promise<Array>} Compactized messages
|
|
212
|
+
*/
|
|
213
|
+
async compactizeConversation(messages, targetModel) {
|
|
214
|
+
const modelSpec = this.modelSpecs[targetModel];
|
|
215
|
+
if (!modelSpec) {
|
|
216
|
+
throw new Error(`Unknown model: ${targetModel}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const maxTokens = modelSpec.contextWindow * 0.8; // Use 80% of context window
|
|
220
|
+
let currentTokens = 0;
|
|
221
|
+
const compactizedMessages = [];
|
|
222
|
+
|
|
223
|
+
// Estimate tokens for each message
|
|
224
|
+
const messagesWithTokens = await Promise.all(
|
|
225
|
+
messages.map(async (msg) => ({
|
|
226
|
+
...msg,
|
|
227
|
+
estimatedTokens: await this._estimateTokens(msg.content, targetModel)
|
|
228
|
+
}))
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Start from the most recent messages
|
|
232
|
+
const reversedMessages = [...messagesWithTokens].reverse();
|
|
233
|
+
|
|
234
|
+
for (const message of reversedMessages) {
|
|
235
|
+
if (currentTokens + message.estimatedTokens > maxTokens) {
|
|
236
|
+
// If we've exceeded the limit, summarize older messages
|
|
237
|
+
if (compactizedMessages.length === 0) {
|
|
238
|
+
// If even the latest message is too long, truncate it
|
|
239
|
+
const truncatedContent = await this._truncateMessage(message.content, maxTokens);
|
|
240
|
+
compactizedMessages.unshift({
|
|
241
|
+
...message,
|
|
242
|
+
content: truncatedContent,
|
|
243
|
+
estimatedTokens: maxTokens
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
compactizedMessages.unshift(message);
|
|
250
|
+
currentTokens += message.estimatedTokens;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If we have remaining older messages, create a summary
|
|
254
|
+
const remainingMessages = messagesWithTokens.slice(0, -compactizedMessages.length);
|
|
255
|
+
if (remainingMessages.length > 0) {
|
|
256
|
+
const summary = await this._summarizeMessages(remainingMessages, targetModel);
|
|
257
|
+
compactizedMessages.unshift({
|
|
258
|
+
role: 'system',
|
|
259
|
+
content: `Previous conversation summary: ${summary}`,
|
|
260
|
+
timestamp: remainingMessages[0].timestamp,
|
|
261
|
+
type: 'summary',
|
|
262
|
+
estimatedTokens: await this._estimateTokens(summary, targetModel)
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.logger.info(`Conversation compactized for model: ${targetModel}`, {
|
|
267
|
+
originalMessages: messages.length,
|
|
268
|
+
compactizedMessages: compactizedMessages.length,
|
|
269
|
+
estimatedTokens: currentTokens,
|
|
270
|
+
maxTokens
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return compactizedMessages;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Track token usage and costs
|
|
278
|
+
* @param {number} tokens - Number of tokens used
|
|
279
|
+
* @param {number} cost - Cost in dollars
|
|
280
|
+
* @returns {Promise<void>}
|
|
281
|
+
*/
|
|
282
|
+
async trackUsage(agentId, model, tokenUsage, cost) {
|
|
283
|
+
try {
|
|
284
|
+
if (this.budgetService) {
|
|
285
|
+
this.budgetService.trackUsage(agentId, model, tokenUsage);
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
this.logger.error(`Usage tracking failed: ${error.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle HTTP errors with comprehensive error handling
|
|
294
|
+
* @param {Error} error - Error object
|
|
295
|
+
* @param {Object} context - Request context
|
|
296
|
+
* @returns {Promise<void>}
|
|
297
|
+
*/
|
|
298
|
+
async handleHttpError(error, context) {
|
|
299
|
+
const errorType = this.errorHandler?.classifyError?.(error, context);
|
|
300
|
+
|
|
301
|
+
switch (error.status || error.code) {
|
|
302
|
+
case HTTP_STATUS.BAD_REQUEST:
|
|
303
|
+
this.logger.error('Bad request to AI service', { context, error: error.message });
|
|
304
|
+
throw new Error(`Invalid request: ${error.message}`);
|
|
305
|
+
|
|
306
|
+
case HTTP_STATUS.UNAUTHORIZED:
|
|
307
|
+
this.logger.error('Authentication failed with AI service', { context });
|
|
308
|
+
throw new Error('Authentication failed - check API credentials');
|
|
309
|
+
|
|
310
|
+
case HTTP_STATUS.FORBIDDEN:
|
|
311
|
+
this.logger.error('Access forbidden to AI service', { context });
|
|
312
|
+
throw new Error('Access forbidden - insufficient permissions');
|
|
313
|
+
|
|
314
|
+
case HTTP_STATUS.NOT_FOUND:
|
|
315
|
+
this.logger.error('AI service endpoint not found', { context });
|
|
316
|
+
throw new Error('Service endpoint not found');
|
|
317
|
+
|
|
318
|
+
case HTTP_STATUS.TOO_MANY_REQUESTS:
|
|
319
|
+
this.logger.warn('Rate limit exceeded', { context });
|
|
320
|
+
await this._handleRateLimit(context);
|
|
321
|
+
throw new Error('Rate limit exceeded - request queued for retry');
|
|
322
|
+
|
|
323
|
+
case HTTP_STATUS.INTERNAL_SERVER_ERROR:
|
|
324
|
+
case HTTP_STATUS.BAD_GATEWAY:
|
|
325
|
+
case HTTP_STATUS.SERVICE_UNAVAILABLE:
|
|
326
|
+
case HTTP_STATUS.GATEWAY_TIMEOUT:
|
|
327
|
+
this.logger.error('AI service unavailable', { context, status: error.status });
|
|
328
|
+
await this._handleServiceUnavailable(context);
|
|
329
|
+
throw new Error('AI service temporarily unavailable');
|
|
330
|
+
|
|
331
|
+
default:
|
|
332
|
+
this.logger.error('Unknown AI service error', { context, error: error.message });
|
|
333
|
+
throw new Error(`AI service error: ${error.message}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Set API key manager instance
|
|
339
|
+
* @param {ApiKeyManager} apiKeyManager - API key manager instance
|
|
340
|
+
*/
|
|
341
|
+
setApiKeyManager(apiKeyManager) {
|
|
342
|
+
this.apiKeyManager = apiKeyManager;
|
|
343
|
+
|
|
344
|
+
this.logger?.info('API key manager set for AI service', {
|
|
345
|
+
hasManager: !!apiKeyManager
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Set agent pool reference
|
|
351
|
+
* @param {Object} agentPool - Agent pool instance
|
|
352
|
+
*/
|
|
353
|
+
setAgentPool(agentPool) {
|
|
354
|
+
this.agentPool = agentPool;
|
|
355
|
+
|
|
356
|
+
this.logger?.info('Agent pool set for AI service', {
|
|
357
|
+
hasAgentPool: !!agentPool
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Generate image from text prompt using AI models
|
|
363
|
+
* @param {string} prompt - Text description of the image to generate
|
|
364
|
+
* @param {Object} options - Generation options
|
|
365
|
+
* @param {string} options.model - Model to use (e.g., 'flux-1.1-pro', 'dall-e-3')
|
|
366
|
+
* @param {string} options.size - Image size (e.g., '1024x1024', '512x512')
|
|
367
|
+
* @param {string} options.quality - Image quality ('standard' or 'hd')
|
|
368
|
+
* @param {string} options.responseFormat - Response format ('url' or 'b64_json')
|
|
369
|
+
* @param {string} options.sessionId - Session ID for API key retrieval
|
|
370
|
+
* @param {boolean} options.platformProvided - Whether to use platform model
|
|
371
|
+
* @returns {Promise<Object>} Generated image result with URL or base64 data
|
|
372
|
+
*/
|
|
373
|
+
async generateImage(prompt, options = {}) {
|
|
374
|
+
const requestId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
// Check circuit breaker
|
|
378
|
+
if (this._isCircuitBreakerOpen()) {
|
|
379
|
+
throw new Error('Service temporarily unavailable - circuit breaker is open');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate prompt
|
|
383
|
+
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
|
384
|
+
throw new Error('Image generation requires a non-empty text prompt');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Default options
|
|
388
|
+
const model = options.model || 'flux-1.1-pro';
|
|
389
|
+
const size = options.size || '1024x1024';
|
|
390
|
+
const quality = options.quality || 'standard';
|
|
391
|
+
const responseFormat = options.responseFormat || 'url';
|
|
392
|
+
|
|
393
|
+
this.logger.info(`Generating image with model: ${model}`, {
|
|
394
|
+
requestId,
|
|
395
|
+
model,
|
|
396
|
+
size,
|
|
397
|
+
quality,
|
|
398
|
+
promptLength: prompt.length
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Prepare request payload
|
|
402
|
+
const payload = {
|
|
403
|
+
prompt,
|
|
404
|
+
model,
|
|
405
|
+
size,
|
|
406
|
+
quality,
|
|
407
|
+
response_format: responseFormat,
|
|
408
|
+
n: 1, // Generate 1 image
|
|
409
|
+
metadata: {
|
|
410
|
+
requestId,
|
|
411
|
+
timestamp: new Date().toISOString()
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// Make API request to image generation endpoint
|
|
416
|
+
const response = await this._makeImageAPIRequest(payload, requestId, {
|
|
417
|
+
sessionId: options.sessionId,
|
|
418
|
+
platformProvided: options.platformProvided || false
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Reset circuit breaker on success
|
|
422
|
+
this._resetCircuitBreaker();
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
url: response.data?.[0]?.url || null,
|
|
426
|
+
b64_json: response.data?.[0]?.b64_json || null,
|
|
427
|
+
model: response.model || model,
|
|
428
|
+
requestId,
|
|
429
|
+
revisedPrompt: response.data?.[0]?.revised_prompt || prompt
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
} catch (error) {
|
|
433
|
+
// Handle circuit breaker
|
|
434
|
+
this._recordFailure();
|
|
435
|
+
|
|
436
|
+
this.logger.error(`Image generation failed: ${error.message}`, {
|
|
437
|
+
requestId,
|
|
438
|
+
model: options.model,
|
|
439
|
+
error: error.stack
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Check service health for circuit breaker
|
|
448
|
+
* @returns {Promise<boolean>} Service health status
|
|
449
|
+
*/
|
|
450
|
+
async checkServiceHealth() {
|
|
451
|
+
try {
|
|
452
|
+
const response = await this._makeAPIRequest('/health', {}, 'health-check');
|
|
453
|
+
return response.status === 'healthy';
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Switch agent to different model
|
|
461
|
+
* @param {string} agentId - Agent identifier
|
|
462
|
+
* @param {string} newModel - New model name
|
|
463
|
+
* @returns {Promise<Object>} Switch result
|
|
464
|
+
*/
|
|
465
|
+
async switchAgentModel(agentId, newModel) {
|
|
466
|
+
try {
|
|
467
|
+
if (!this._isValidModel(newModel)) {
|
|
468
|
+
throw new Error(`Invalid model: ${newModel}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Get conversation manager for agent
|
|
472
|
+
let conversationManager = this.conversationManagers.get(agentId);
|
|
473
|
+
if (!conversationManager) {
|
|
474
|
+
// Create new conversation manager if it doesn't exist
|
|
475
|
+
conversationManager = new ConversationManager(agentId, this.logger);
|
|
476
|
+
this.conversationManagers.set(agentId, conversationManager);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Switch model and return conversation
|
|
480
|
+
const modelConversation = await conversationManager.switchModel(newModel);
|
|
481
|
+
|
|
482
|
+
// CRITICAL FIX: Update agent's currentModel field in AgentPool
|
|
483
|
+
const agent = await this.agentPool?.getAgent(agentId);
|
|
484
|
+
if (agent) {
|
|
485
|
+
agent.currentModel = newModel;
|
|
486
|
+
await this.agentPool.persistAgentState(agentId);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
this.logger.info(`Agent model switched: ${agentId}`, {
|
|
490
|
+
newModel,
|
|
491
|
+
messageCount: modelConversation.messages.length
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
success: true,
|
|
496
|
+
agentId,
|
|
497
|
+
newModel,
|
|
498
|
+
conversation: modelConversation
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.logger.error(`Model switch failed: ${error.message}`, { agentId, newModel });
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Initialize model specifications
|
|
509
|
+
* @private
|
|
510
|
+
*/
|
|
511
|
+
_initializeModelSpecs() {
|
|
512
|
+
const baseSpecs = {
|
|
513
|
+
// Anthropic Claude models
|
|
514
|
+
[MODELS.ANTHROPIC_SONNET]: {
|
|
515
|
+
provider: MODEL_PROVIDERS.ANTHROPIC,
|
|
516
|
+
contextWindow: 200000,
|
|
517
|
+
maxTokens: 8192, // Increased from 4096
|
|
518
|
+
costPer1kTokens: 0.015
|
|
519
|
+
},
|
|
520
|
+
[MODELS.ANTHROPIC_OPUS]: {
|
|
521
|
+
provider: MODEL_PROVIDERS.ANTHROPIC,
|
|
522
|
+
contextWindow: 200000,
|
|
523
|
+
maxTokens: 8192, // Increased from 4096
|
|
524
|
+
costPer1kTokens: 0.075
|
|
525
|
+
},
|
|
526
|
+
[MODELS.ANTHROPIC_HAIKU]: {
|
|
527
|
+
provider: MODEL_PROVIDERS.ANTHROPIC,
|
|
528
|
+
contextWindow: 200000,
|
|
529
|
+
maxTokens: 8192, // Increased from 4096
|
|
530
|
+
costPer1kTokens: 0.0025
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
// OpenAI models
|
|
534
|
+
[MODELS.GPT_4]: {
|
|
535
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
536
|
+
contextWindow: 128000,
|
|
537
|
+
maxTokens: 8192, // Increased from 4096
|
|
538
|
+
costPer1kTokens: 0.03
|
|
539
|
+
},
|
|
540
|
+
[MODELS.GPT_4_MINI]: {
|
|
541
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
542
|
+
contextWindow: 128000,
|
|
543
|
+
maxTokens: 16384,
|
|
544
|
+
costPer1kTokens: 0.0015
|
|
545
|
+
},
|
|
546
|
+
'gpt-4o': {
|
|
547
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
548
|
+
contextWindow: 128000,
|
|
549
|
+
maxTokens: 8192,
|
|
550
|
+
costPer1kTokens: 0.03
|
|
551
|
+
},
|
|
552
|
+
'gpt-4o-mini': {
|
|
553
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
554
|
+
contextWindow: 128000,
|
|
555
|
+
maxTokens: 16384,
|
|
556
|
+
costPer1kTokens: 0.0015
|
|
557
|
+
},
|
|
558
|
+
'gpt-4-turbo': {
|
|
559
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
560
|
+
contextWindow: 128000,
|
|
561
|
+
maxTokens: 8192,
|
|
562
|
+
costPer1kTokens: 0.03
|
|
563
|
+
},
|
|
564
|
+
'gpt-3.5-turbo': {
|
|
565
|
+
provider: MODEL_PROVIDERS.OPENAI,
|
|
566
|
+
contextWindow: 16384,
|
|
567
|
+
maxTokens: 4096,
|
|
568
|
+
costPer1kTokens: 0.001
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
// DeepSeek models
|
|
572
|
+
[MODELS.DEEPSEEK_R1]: {
|
|
573
|
+
provider: MODEL_PROVIDERS.DEEPSEEK,
|
|
574
|
+
contextWindow: 128000,
|
|
575
|
+
maxTokens: 8192,
|
|
576
|
+
costPer1kTokens: 0.002
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
// Phi models
|
|
580
|
+
[MODELS.PHI_4]: {
|
|
581
|
+
provider: MODEL_PROVIDERS.PHI,
|
|
582
|
+
contextWindow: 16384,
|
|
583
|
+
maxTokens: 4096, // Increased from 2048
|
|
584
|
+
costPer1kTokens: 0.001
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
// Azure AI Foundry models
|
|
588
|
+
'azure-ai-grok3': {
|
|
589
|
+
provider: 'AZURE',
|
|
590
|
+
contextWindow: 128000,
|
|
591
|
+
maxTokens: 8192, // Increased from 4096
|
|
592
|
+
costPer1kTokens: 0.01
|
|
593
|
+
},
|
|
594
|
+
'azure-ai-deepseek-r1': {
|
|
595
|
+
provider: 'AZURE',
|
|
596
|
+
contextWindow: 128000,
|
|
597
|
+
maxTokens: 8192,
|
|
598
|
+
costPer1kTokens: 0.002
|
|
599
|
+
},
|
|
600
|
+
'azure-openai-gpt-5': {
|
|
601
|
+
provider: 'AZURE',
|
|
602
|
+
contextWindow: 128000,
|
|
603
|
+
maxTokens: 8192,
|
|
604
|
+
costPer1kTokens: 0.03
|
|
605
|
+
},
|
|
606
|
+
'azure-openai-gpt-4': {
|
|
607
|
+
provider: 'AZURE',
|
|
608
|
+
contextWindow: 128000,
|
|
609
|
+
maxTokens: 8192,
|
|
610
|
+
costPer1kTokens: 0.03
|
|
611
|
+
},
|
|
612
|
+
'azure-openai-gpt-4o': {
|
|
613
|
+
provider: 'AZURE',
|
|
614
|
+
contextWindow: 128000,
|
|
615
|
+
maxTokens: 8192,
|
|
616
|
+
costPer1kTokens: 0.03
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
// Router model
|
|
620
|
+
'autopilot-model-router': {
|
|
621
|
+
provider: 'AZURE',
|
|
622
|
+
contextWindow: 16384,
|
|
623
|
+
maxTokens: 2048,
|
|
624
|
+
costPer1kTokens: 0.001
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// No need for prefixed models anymore - just return clean base specs
|
|
629
|
+
return baseSpecs;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Format messages for specific model
|
|
634
|
+
* @private
|
|
635
|
+
*/
|
|
636
|
+
_formatMessagesForModel(messages, model, options) {
|
|
637
|
+
// Get model spec or use default
|
|
638
|
+
const modelSpec = this.modelSpecs[model] || { provider: 'AZURE' };
|
|
639
|
+
|
|
640
|
+
let formattedMessages;
|
|
641
|
+
|
|
642
|
+
if (typeof messages === 'string') {
|
|
643
|
+
// Single message
|
|
644
|
+
formattedMessages = [{
|
|
645
|
+
role: 'user',
|
|
646
|
+
content: messages
|
|
647
|
+
}];
|
|
648
|
+
} else {
|
|
649
|
+
// Message array
|
|
650
|
+
formattedMessages = messages.map(msg => this._formatSingleMessage(msg, model));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Apply provider-specific formatting
|
|
654
|
+
switch (modelSpec.provider) {
|
|
655
|
+
case MODEL_PROVIDERS.ANTHROPIC:
|
|
656
|
+
return this._formatForAnthropic(formattedMessages);
|
|
657
|
+
case MODEL_PROVIDERS.OPENAI:
|
|
658
|
+
return this._formatForOpenAI(formattedMessages);
|
|
659
|
+
case MODEL_PROVIDERS.AZURE:
|
|
660
|
+
return this._formatForAzure(formattedMessages);
|
|
661
|
+
default:
|
|
662
|
+
return formattedMessages;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Format single message for model
|
|
668
|
+
* @private
|
|
669
|
+
*/
|
|
670
|
+
_formatSingleMessage(message, model) {
|
|
671
|
+
return {
|
|
672
|
+
role: message.role || 'user',
|
|
673
|
+
content: message.content,
|
|
674
|
+
timestamp: message.timestamp
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Format messages for Anthropic models
|
|
680
|
+
* @private
|
|
681
|
+
*/
|
|
682
|
+
_formatForAnthropic(messages) {
|
|
683
|
+
return messages.map(msg => {
|
|
684
|
+
if (msg.role === 'system') {
|
|
685
|
+
return {
|
|
686
|
+
role: 'user',
|
|
687
|
+
content: `System: ${msg.content}`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return msg;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Format messages for OpenAI models
|
|
696
|
+
* @private
|
|
697
|
+
*/
|
|
698
|
+
_formatForOpenAI(messages) {
|
|
699
|
+
// OpenAI supports system role natively
|
|
700
|
+
return messages;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Format messages for Azure models
|
|
705
|
+
* @private
|
|
706
|
+
*/
|
|
707
|
+
_formatForAzure(messages) {
|
|
708
|
+
// Azure may have specific formatting requirements
|
|
709
|
+
return messages.map(msg => ({
|
|
710
|
+
...msg,
|
|
711
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Check if model is valid
|
|
717
|
+
* @private
|
|
718
|
+
*/
|
|
719
|
+
_isValidModel(model) {
|
|
720
|
+
this.logger.debug('Validating model', { model, modelType: typeof model });
|
|
721
|
+
|
|
722
|
+
// Check if model exists in our specs directly
|
|
723
|
+
if (this.modelSpecs[model] !== undefined) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
this.logger.warn('Model validation failed', {
|
|
728
|
+
model,
|
|
729
|
+
availableModels: Object.keys(this.modelSpecs)
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Check model health status
|
|
737
|
+
* @private
|
|
738
|
+
*/
|
|
739
|
+
async _checkModelHealth(model) {
|
|
740
|
+
// Implementation would check model-specific health endpoints
|
|
741
|
+
// For now, return true (assuming all models are healthy)
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Estimate tokens for content
|
|
747
|
+
* @private
|
|
748
|
+
*/
|
|
749
|
+
async _estimateTokens(content, model) {
|
|
750
|
+
// Rough estimation: 1 token ≈ 4 characters for most models
|
|
751
|
+
return Math.ceil(content.length / 4);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Truncate message to fit token limit
|
|
756
|
+
* @private
|
|
757
|
+
*/
|
|
758
|
+
async _truncateMessage(content, maxTokens) {
|
|
759
|
+
const maxChars = maxTokens * 4; // Rough estimation
|
|
760
|
+
if (content.length <= maxChars) {
|
|
761
|
+
return content;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return content.substring(0, maxChars - 20) + '\n... [message truncated]';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Summarize messages for compactization
|
|
769
|
+
* @private
|
|
770
|
+
*/
|
|
771
|
+
async _summarizeMessages(messages, model) {
|
|
772
|
+
const combinedContent = messages
|
|
773
|
+
.map(msg => `${msg.role}: ${msg.content}`)
|
|
774
|
+
.join('\n');
|
|
775
|
+
|
|
776
|
+
// This would use the AI service to create a summary
|
|
777
|
+
// For now, return a simple truncated version
|
|
778
|
+
const maxLength = 500;
|
|
779
|
+
if (combinedContent.length <= maxLength) {
|
|
780
|
+
return combinedContent;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return combinedContent.substring(0, maxLength) + '... [conversation summary truncated]';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Make API request with retry logic
|
|
788
|
+
* @private
|
|
789
|
+
*/
|
|
790
|
+
async _makeAPIRequest(endpoint, payload, requestId, options = {}) {
|
|
791
|
+
// Make request directly to Azure backend (not through local proxy)
|
|
792
|
+
const azureBackendUrl = 'https://autopilot-api.azurewebsites.net/llm/chat';
|
|
793
|
+
let lastError;
|
|
794
|
+
|
|
795
|
+
// Get API keys from session-based storage
|
|
796
|
+
let apiKey = null;
|
|
797
|
+
let vendorApiKey = null;
|
|
798
|
+
|
|
799
|
+
// Log the state for debugging
|
|
800
|
+
this.logger?.info('🔑 API Key retrieval state', {
|
|
801
|
+
hasApiKeyManager: !!this.apiKeyManager,
|
|
802
|
+
sessionId: options.sessionId,
|
|
803
|
+
hasSessionId: !!options.sessionId,
|
|
804
|
+
optionsKeys: Object.keys(options),
|
|
805
|
+
model: payload.model
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// First try to get from API key manager using session ID
|
|
809
|
+
if (this.apiKeyManager && options.sessionId) {
|
|
810
|
+
const keys = this.apiKeyManager.getKeysForRequest(options.sessionId, {
|
|
811
|
+
platformProvided: options.platformProvided || false,
|
|
812
|
+
vendor: this._getVendorFromModel(payload.model)
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
apiKey = keys.loxiaApiKey;
|
|
816
|
+
vendorApiKey = keys.vendorApiKey;
|
|
817
|
+
|
|
818
|
+
this.logger?.debug('Retrieved API keys from session manager', {
|
|
819
|
+
sessionId: options.sessionId,
|
|
820
|
+
hasLoxiaKey: !!apiKey,
|
|
821
|
+
hasVendorKey: !!vendorApiKey,
|
|
822
|
+
vendor: this._getVendorFromModel(payload.model)
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Fallback to options (passed from frontend)
|
|
827
|
+
if (!apiKey && options.apiKey) {
|
|
828
|
+
apiKey = options.apiKey;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Fallback to config if no API key from session or options
|
|
832
|
+
if (!apiKey && this.config.apiKey) {
|
|
833
|
+
apiKey = this.config.apiKey;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (!apiKey) {
|
|
837
|
+
throw new Error('No API key configured. Please configure your Loxia API key in Settings.');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Use the model name from payload (already transformed)
|
|
841
|
+
const modelName = payload.model;
|
|
842
|
+
|
|
843
|
+
// Transform the payload to match the Azure backend API format
|
|
844
|
+
const azurePayload = {
|
|
845
|
+
conversationId: requestId,
|
|
846
|
+
message: payload.messages[payload.messages.length - 1]?.content || '',
|
|
847
|
+
messages: payload.messages,
|
|
848
|
+
model: modelName,
|
|
849
|
+
requestId,
|
|
850
|
+
options: payload.options || {},
|
|
851
|
+
platformProvided: options.platformProvided || false // Indicate if this is a platform model
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Add system prompt if provided
|
|
855
|
+
if (payload.system) {
|
|
856
|
+
azurePayload.systemPrompt = payload.system;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Include appropriate API key based on model type
|
|
860
|
+
if (options.platformProvided) {
|
|
861
|
+
// Platform models use Loxia API key
|
|
862
|
+
azurePayload.apiKey = apiKey;
|
|
863
|
+
} else {
|
|
864
|
+
// Direct access models use vendor-specific keys
|
|
865
|
+
if (vendorApiKey) {
|
|
866
|
+
azurePayload.vendorApiKey = vendorApiKey;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Also include custom API keys from options for backward compatibility
|
|
870
|
+
if (options.customApiKeys) {
|
|
871
|
+
azurePayload.customApiKeys = options.customApiKeys;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Include Loxia API key as fallback
|
|
875
|
+
azurePayload.apiKey = apiKey;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
879
|
+
try {
|
|
880
|
+
this.logger.info('Making request to Azure backend', {
|
|
881
|
+
url: azureBackendUrl,
|
|
882
|
+
model: payload.model,
|
|
883
|
+
requestId,
|
|
884
|
+
attempt,
|
|
885
|
+
hasApiKey: !!apiKey
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const response = await fetch(azureBackendUrl, {
|
|
889
|
+
method: 'POST',
|
|
890
|
+
headers: {
|
|
891
|
+
'Content-Type': 'application/json',
|
|
892
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
893
|
+
'X-Request-ID': requestId
|
|
894
|
+
},
|
|
895
|
+
body: JSON.stringify(azurePayload),
|
|
896
|
+
timeout: this.timeout
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
if (!response.ok) {
|
|
900
|
+
const errorText = await response.text();
|
|
901
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
|
|
902
|
+
error.status = response.status;
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const data = await response.json();
|
|
907
|
+
|
|
908
|
+
// Transform Azure backend response to match expected format
|
|
909
|
+
return {
|
|
910
|
+
choices: [{
|
|
911
|
+
message: {
|
|
912
|
+
content: data.content || '',
|
|
913
|
+
role: 'assistant'
|
|
914
|
+
},
|
|
915
|
+
finish_reason: data.finishReason || 'stop'
|
|
916
|
+
}],
|
|
917
|
+
usage: data.usage || {
|
|
918
|
+
prompt_tokens: 0,
|
|
919
|
+
completion_tokens: 0,
|
|
920
|
+
total_tokens: 0
|
|
921
|
+
},
|
|
922
|
+
model: data.model || payload.model,
|
|
923
|
+
id: data.requestId || requestId
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
} catch (error) {
|
|
927
|
+
lastError = error;
|
|
928
|
+
|
|
929
|
+
this.logger.warn('Request to Azure backend failed', {
|
|
930
|
+
attempt,
|
|
931
|
+
requestId,
|
|
932
|
+
error: error.message,
|
|
933
|
+
status: error.status
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (attempt < this.retryAttempts) {
|
|
937
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
938
|
+
this.logger.warn(`Retrying in ${delay}ms`, { attempt, requestId });
|
|
939
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// If all attempts failed, return a mock response with error info
|
|
945
|
+
this.logger.error('All attempts to reach backend failed, using mock response', {
|
|
946
|
+
requestId,
|
|
947
|
+
error: lastError.message
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Check if this is an API key error that should stop retries
|
|
951
|
+
const isApiKeyError = lastError.message && lastError.message.includes('No API key configured');
|
|
952
|
+
|
|
953
|
+
// For API key errors, throw to stop the autonomous loop
|
|
954
|
+
if (isApiKeyError) {
|
|
955
|
+
throw new Error(`API Configuration Error: ${lastError.message}`);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// For other errors, return a short error message without including the original content
|
|
959
|
+
return {
|
|
960
|
+
choices: [{
|
|
961
|
+
message: {
|
|
962
|
+
content: `I apologize, but I'm unable to connect to the AI service at the moment. Error: ${lastError.message}`,
|
|
963
|
+
role: 'assistant'
|
|
964
|
+
},
|
|
965
|
+
finish_reason: 'stop'
|
|
966
|
+
}],
|
|
967
|
+
usage: {
|
|
968
|
+
prompt_tokens: 0,
|
|
969
|
+
completion_tokens: 0,
|
|
970
|
+
total_tokens: 0
|
|
971
|
+
},
|
|
972
|
+
model: payload.model,
|
|
973
|
+
id: `error-${requestId}`,
|
|
974
|
+
error: lastError.message
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Make API request for image generation
|
|
981
|
+
* @private
|
|
982
|
+
*/
|
|
983
|
+
async _makeImageAPIRequest(payload, requestId, options = {}) {
|
|
984
|
+
// Image generation endpoint on Azure backend (CORRECTED)
|
|
985
|
+
const azureImageUrl = 'https://autopilot-api.azurewebsites.net/llm/generate-image';
|
|
986
|
+
let lastError;
|
|
987
|
+
|
|
988
|
+
// Get API keys from session-based storage
|
|
989
|
+
let apiKey = null;
|
|
990
|
+
let vendorApiKey = null;
|
|
991
|
+
|
|
992
|
+
this.logger?.info('🖼️ Image API request state', {
|
|
993
|
+
hasApiKeyManager: !!this.apiKeyManager,
|
|
994
|
+
sessionId: options.sessionId,
|
|
995
|
+
hasSessionId: !!options.sessionId,
|
|
996
|
+
model: payload.model
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// First try to get from API key manager using session ID
|
|
1000
|
+
if (this.apiKeyManager && options.sessionId) {
|
|
1001
|
+
const keys = this.apiKeyManager.getKeysForRequest(options.sessionId, {
|
|
1002
|
+
platformProvided: options.platformProvided || false,
|
|
1003
|
+
vendor: this._getVendorFromModel(payload.model)
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
apiKey = keys.loxiaApiKey;
|
|
1007
|
+
vendorApiKey = keys.vendorApiKey;
|
|
1008
|
+
|
|
1009
|
+
this.logger?.debug('Retrieved API keys from session manager for image', {
|
|
1010
|
+
sessionId: options.sessionId,
|
|
1011
|
+
hasLoxiaKey: !!apiKey,
|
|
1012
|
+
hasVendorKey: !!vendorApiKey
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Fallback to options (passed from frontend)
|
|
1017
|
+
if (!apiKey && options.apiKey) {
|
|
1018
|
+
apiKey = options.apiKey;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Fallback to config if no API key from session or options
|
|
1022
|
+
if (!apiKey && this.config.apiKey) {
|
|
1023
|
+
apiKey = this.config.apiKey;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (!apiKey) {
|
|
1027
|
+
throw new Error('No API key configured. Please configure your Loxia API key in Settings.');
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Transform the payload to match Azure backend format
|
|
1031
|
+
// Backend expects: prompt, model, size, quality, style, n, requestId
|
|
1032
|
+
const azurePayload = {
|
|
1033
|
+
prompt: payload.prompt,
|
|
1034
|
+
model: payload.model || 'azure-openai-dalle3', // Backend default
|
|
1035
|
+
size: payload.size,
|
|
1036
|
+
quality: payload.quality,
|
|
1037
|
+
style: payload.style || 'vivid', // Backend default
|
|
1038
|
+
n: payload.n || 1,
|
|
1039
|
+
requestId
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
// API key is sent via Authorization header, not in body
|
|
1043
|
+
|
|
1044
|
+
for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
|
|
1045
|
+
try {
|
|
1046
|
+
this.logger.info('Making image request to Azure backend', {
|
|
1047
|
+
url: azureImageUrl,
|
|
1048
|
+
model: payload.model,
|
|
1049
|
+
requestId,
|
|
1050
|
+
attempt,
|
|
1051
|
+
hasApiKey: !!apiKey
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
const response = await fetch(azureImageUrl, {
|
|
1055
|
+
method: 'POST',
|
|
1056
|
+
headers: {
|
|
1057
|
+
'Content-Type': 'application/json',
|
|
1058
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1059
|
+
'X-Request-ID': requestId
|
|
1060
|
+
},
|
|
1061
|
+
body: JSON.stringify(azurePayload),
|
|
1062
|
+
timeout: this.timeout * 2 // Image generation may take longer
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
if (!response.ok) {
|
|
1066
|
+
const errorText = await response.text();
|
|
1067
|
+
const error = new Error(`HTTP ${response.status}: ${response.statusText}${errorText ? ` - ${errorText}` : ''}`);
|
|
1068
|
+
error.status = response.status;
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const data = await response.json();
|
|
1073
|
+
|
|
1074
|
+
// Backend returns: { images: [...], usage: {...}, model, requestId, created }
|
|
1075
|
+
// Transform to match our expected format
|
|
1076
|
+
return {
|
|
1077
|
+
data: data.images || [],
|
|
1078
|
+
model: data.model || payload.model,
|
|
1079
|
+
created: data.created || Date.now(),
|
|
1080
|
+
usage: data.usage
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
lastError = error;
|
|
1085
|
+
|
|
1086
|
+
this.logger.warn('Image request to Azure backend failed', {
|
|
1087
|
+
attempt,
|
|
1088
|
+
requestId,
|
|
1089
|
+
error: error.message,
|
|
1090
|
+
status: error.status
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (attempt < this.retryAttempts) {
|
|
1094
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
|
1095
|
+
this.logger.warn(`Retrying in ${delay}ms`, { attempt, requestId });
|
|
1096
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// If all attempts failed, throw error
|
|
1102
|
+
this.logger.error('All image generation attempts failed', {
|
|
1103
|
+
requestId,
|
|
1104
|
+
error: lastError.message
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
throw new Error(`Image generation failed: ${lastError.message}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Check rate limits
|
|
1112
|
+
* @private
|
|
1113
|
+
*/
|
|
1114
|
+
async _checkRateLimit(model) {
|
|
1115
|
+
// Implementation would check model-specific rate limits
|
|
1116
|
+
// For now, just add a small delay
|
|
1117
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* Handle rate limit exceeded
|
|
1122
|
+
* @private
|
|
1123
|
+
*/
|
|
1124
|
+
async _handleRateLimit(context) {
|
|
1125
|
+
const delay = 60000; // 1 minute delay for rate limits
|
|
1126
|
+
this.logger.info(`Rate limit exceeded, waiting ${delay}ms`, context);
|
|
1127
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Handle service unavailable
|
|
1132
|
+
* @private
|
|
1133
|
+
*/
|
|
1134
|
+
async _handleServiceUnavailable(context) {
|
|
1135
|
+
this._recordFailure();
|
|
1136
|
+
const delay = 30000; // 30 second delay for service issues
|
|
1137
|
+
this.logger.info(`Service unavailable, waiting ${delay}ms`, context);
|
|
1138
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Check if circuit breaker is open
|
|
1143
|
+
* @private
|
|
1144
|
+
*/
|
|
1145
|
+
_isCircuitBreakerOpen() {
|
|
1146
|
+
if (!this.circuitBreaker.isOpen) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const timeSinceLastFailure = Date.now() - this.circuitBreaker.lastFailureTime;
|
|
1151
|
+
if (timeSinceLastFailure > this.circuitBreaker.timeout) {
|
|
1152
|
+
this.circuitBreaker.isOpen = false;
|
|
1153
|
+
this.circuitBreaker.failures = 0;
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return true;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Record failure for circuit breaker
|
|
1162
|
+
* @private
|
|
1163
|
+
*/
|
|
1164
|
+
_recordFailure() {
|
|
1165
|
+
this.circuitBreaker.failures++;
|
|
1166
|
+
this.circuitBreaker.lastFailureTime = Date.now();
|
|
1167
|
+
|
|
1168
|
+
if (this.circuitBreaker.failures >= this.circuitBreaker.threshold) {
|
|
1169
|
+
this.circuitBreaker.isOpen = true;
|
|
1170
|
+
this.logger.warn('Circuit breaker opened due to repeated failures');
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Reset circuit breaker on success
|
|
1176
|
+
* @private
|
|
1177
|
+
*/
|
|
1178
|
+
_resetCircuitBreaker() {
|
|
1179
|
+
if (this.circuitBreaker.failures > 0) {
|
|
1180
|
+
this.circuitBreaker.failures = 0;
|
|
1181
|
+
this.circuitBreaker.isOpen = false;
|
|
1182
|
+
this.logger.info('Circuit breaker reset - service recovered');
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Extract vendor name from model name
|
|
1188
|
+
* @param {string} model - Model name
|
|
1189
|
+
* @returns {string|null} Vendor name
|
|
1190
|
+
* @private
|
|
1191
|
+
*/
|
|
1192
|
+
_getVendorFromModel(model) {
|
|
1193
|
+
if (!model) return null;
|
|
1194
|
+
|
|
1195
|
+
const modelName = model.toLowerCase();
|
|
1196
|
+
|
|
1197
|
+
if (modelName.includes('anthropic') || modelName.includes('claude')) {
|
|
1198
|
+
return 'anthropic';
|
|
1199
|
+
} else if (modelName.includes('openai') || modelName.includes('gpt')) {
|
|
1200
|
+
return 'openai';
|
|
1201
|
+
} else if (modelName.includes('deepseek')) {
|
|
1202
|
+
return 'deepseek';
|
|
1203
|
+
} else if (modelName.includes('phi')) {
|
|
1204
|
+
return 'microsoft';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* ConversationManager - Handles multi-model conversation state
|
|
1213
|
+
*/
|
|
1214
|
+
class ConversationManager {
|
|
1215
|
+
constructor(agentId, logger) {
|
|
1216
|
+
this.agentId = agentId;
|
|
1217
|
+
this.logger = logger;
|
|
1218
|
+
this.conversations = new Map();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async switchModel(newModel) {
|
|
1222
|
+
// Implementation would handle model switching logic
|
|
1223
|
+
// For now, return empty conversation
|
|
1224
|
+
return {
|
|
1225
|
+
messages: [],
|
|
1226
|
+
model: newModel,
|
|
1227
|
+
lastUpdated: new Date().toISOString()
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export default AIService;
|