@superdangerous/app-framework 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +652 -0
  3. package/dist/api/logsRouter.d.ts +20 -0
  4. package/dist/api/logsRouter.d.ts.map +1 -0
  5. package/dist/api/logsRouter.js +515 -0
  6. package/dist/api/logsRouter.js.map +1 -0
  7. package/dist/cli/dev-server.d.ts +7 -0
  8. package/dist/cli/dev-server.d.ts.map +1 -0
  9. package/dist/cli/dev-server.js +640 -0
  10. package/dist/cli/dev-server.js.map +1 -0
  11. package/dist/cli/index.d.ts +7 -0
  12. package/dist/cli/index.d.ts.map +1 -0
  13. package/dist/cli/index.js +26 -0
  14. package/dist/cli/index.js.map +1 -0
  15. package/dist/core/StandardServer.d.ts +129 -0
  16. package/dist/core/StandardServer.d.ts.map +1 -0
  17. package/dist/core/StandardServer.js +453 -0
  18. package/dist/core/StandardServer.js.map +1 -0
  19. package/dist/core/apiResponse.d.ts +69 -0
  20. package/dist/core/apiResponse.d.ts.map +1 -0
  21. package/dist/core/apiResponse.js +127 -0
  22. package/dist/core/apiResponse.js.map +1 -0
  23. package/dist/core/healthCheck.d.ts +160 -0
  24. package/dist/core/healthCheck.d.ts.map +1 -0
  25. package/dist/core/healthCheck.js +398 -0
  26. package/dist/core/healthCheck.js.map +1 -0
  27. package/dist/core/index.d.ts +40 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +40 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/logger.d.ts +117 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/logger.js +826 -0
  34. package/dist/core/logger.js.map +1 -0
  35. package/dist/core/portUtils.d.ts +71 -0
  36. package/dist/core/portUtils.d.ts.map +1 -0
  37. package/dist/core/portUtils.js +240 -0
  38. package/dist/core/portUtils.js.map +1 -0
  39. package/dist/core/storageService.d.ts +119 -0
  40. package/dist/core/storageService.d.ts.map +1 -0
  41. package/dist/core/storageService.js +405 -0
  42. package/dist/core/storageService.js.map +1 -0
  43. package/dist/desktop/bundler.d.ts +40 -0
  44. package/dist/desktop/bundler.d.ts.map +1 -0
  45. package/dist/desktop/bundler.js +176 -0
  46. package/dist/desktop/bundler.js.map +1 -0
  47. package/dist/desktop/index.d.ts +25 -0
  48. package/dist/desktop/index.d.ts.map +1 -0
  49. package/dist/desktop/index.js +15 -0
  50. package/dist/desktop/index.js.map +1 -0
  51. package/dist/desktop/native-modules.d.ts +66 -0
  52. package/dist/desktop/native-modules.d.ts.map +1 -0
  53. package/dist/desktop/native-modules.js +200 -0
  54. package/dist/desktop/native-modules.js.map +1 -0
  55. package/dist/index.d.ts +29 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +39 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/logging/LogCategories.d.ts +87 -0
  60. package/dist/logging/LogCategories.d.ts.map +1 -0
  61. package/dist/logging/LogCategories.js +205 -0
  62. package/dist/logging/LogCategories.js.map +1 -0
  63. package/dist/middleware/aiErrorHandler.d.ts +31 -0
  64. package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
  65. package/dist/middleware/aiErrorHandler.js +181 -0
  66. package/dist/middleware/aiErrorHandler.js.map +1 -0
  67. package/dist/middleware/auth.d.ts +101 -0
  68. package/dist/middleware/auth.d.ts.map +1 -0
  69. package/dist/middleware/auth.js +230 -0
  70. package/dist/middleware/auth.js.map +1 -0
  71. package/dist/middleware/cors.d.ts +56 -0
  72. package/dist/middleware/cors.d.ts.map +1 -0
  73. package/dist/middleware/cors.js +123 -0
  74. package/dist/middleware/cors.js.map +1 -0
  75. package/dist/middleware/errorHandler.d.ts +13 -0
  76. package/dist/middleware/errorHandler.d.ts.map +1 -0
  77. package/dist/middleware/errorHandler.js +85 -0
  78. package/dist/middleware/errorHandler.js.map +1 -0
  79. package/dist/middleware/fileUpload.d.ts +62 -0
  80. package/dist/middleware/fileUpload.d.ts.map +1 -0
  81. package/dist/middleware/fileUpload.js +175 -0
  82. package/dist/middleware/fileUpload.js.map +1 -0
  83. package/dist/middleware/health.d.ts +48 -0
  84. package/dist/middleware/health.d.ts.map +1 -0
  85. package/dist/middleware/health.js +143 -0
  86. package/dist/middleware/health.js.map +1 -0
  87. package/dist/middleware/index.d.ts +20 -0
  88. package/dist/middleware/index.d.ts.map +1 -0
  89. package/dist/middleware/index.js +18 -0
  90. package/dist/middleware/index.js.map +1 -0
  91. package/dist/middleware/openapi.d.ts +64 -0
  92. package/dist/middleware/openapi.d.ts.map +1 -0
  93. package/dist/middleware/openapi.js +258 -0
  94. package/dist/middleware/openapi.js.map +1 -0
  95. package/dist/middleware/requestLogging.d.ts +22 -0
  96. package/dist/middleware/requestLogging.d.ts.map +1 -0
  97. package/dist/middleware/requestLogging.js +61 -0
  98. package/dist/middleware/requestLogging.js.map +1 -0
  99. package/dist/middleware/session.d.ts +84 -0
  100. package/dist/middleware/session.d.ts.map +1 -0
  101. package/dist/middleware/session.js +189 -0
  102. package/dist/middleware/session.js.map +1 -0
  103. package/dist/middleware/validation.d.ts +1337 -0
  104. package/dist/middleware/validation.d.ts.map +1 -0
  105. package/dist/middleware/validation.js +483 -0
  106. package/dist/middleware/validation.js.map +1 -0
  107. package/dist/services/aiService.d.ts +180 -0
  108. package/dist/services/aiService.d.ts.map +1 -0
  109. package/dist/services/aiService.js +547 -0
  110. package/dist/services/aiService.js.map +1 -0
  111. package/dist/services/conversationStorage.d.ts +38 -0
  112. package/dist/services/conversationStorage.d.ts.map +1 -0
  113. package/dist/services/conversationStorage.js +158 -0
  114. package/dist/services/conversationStorage.js.map +1 -0
  115. package/dist/services/crossPlatformBuffer.d.ts +84 -0
  116. package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
  117. package/dist/services/crossPlatformBuffer.js +246 -0
  118. package/dist/services/crossPlatformBuffer.js.map +1 -0
  119. package/dist/services/index.d.ts +17 -0
  120. package/dist/services/index.d.ts.map +1 -0
  121. package/dist/services/index.js +18 -0
  122. package/dist/services/index.js.map +1 -0
  123. package/dist/services/networkService.d.ts +81 -0
  124. package/dist/services/networkService.d.ts.map +1 -0
  125. package/dist/services/networkService.js +268 -0
  126. package/dist/services/networkService.js.map +1 -0
  127. package/dist/services/queueService.d.ts +112 -0
  128. package/dist/services/queueService.d.ts.map +1 -0
  129. package/dist/services/queueService.js +338 -0
  130. package/dist/services/queueService.js.map +1 -0
  131. package/dist/services/settingsService.d.ts +135 -0
  132. package/dist/services/settingsService.d.ts.map +1 -0
  133. package/dist/services/settingsService.js +425 -0
  134. package/dist/services/settingsService.js.map +1 -0
  135. package/dist/services/systemMonitor.d.ts +208 -0
  136. package/dist/services/systemMonitor.d.ts.map +1 -0
  137. package/dist/services/systemMonitor.js +693 -0
  138. package/dist/services/systemMonitor.js.map +1 -0
  139. package/dist/services/updateService.d.ts +78 -0
  140. package/dist/services/updateService.d.ts.map +1 -0
  141. package/dist/services/updateService.js +252 -0
  142. package/dist/services/updateService.js.map +1 -0
  143. package/dist/services/websocketEvents.d.ts +372 -0
  144. package/dist/services/websocketEvents.d.ts.map +1 -0
  145. package/dist/services/websocketEvents.js +338 -0
  146. package/dist/services/websocketEvents.js.map +1 -0
  147. package/dist/services/websocketServer.d.ts +80 -0
  148. package/dist/services/websocketServer.d.ts.map +1 -0
  149. package/dist/services/websocketServer.js +299 -0
  150. package/dist/services/websocketServer.js.map +1 -0
  151. package/dist/settings/SettingsSchema.d.ts +151 -0
  152. package/dist/settings/SettingsSchema.d.ts.map +1 -0
  153. package/dist/settings/SettingsSchema.js +424 -0
  154. package/dist/settings/SettingsSchema.js.map +1 -0
  155. package/dist/testing/TestServer.d.ts +69 -0
  156. package/dist/testing/TestServer.d.ts.map +1 -0
  157. package/dist/testing/TestServer.js +250 -0
  158. package/dist/testing/TestServer.js.map +1 -0
  159. package/dist/types/index.d.ts +137 -0
  160. package/dist/types/index.d.ts.map +1 -0
  161. package/dist/types/index.js +5 -0
  162. package/dist/types/index.js.map +1 -0
  163. package/dist/utils/appPaths.d.ts +74 -0
  164. package/dist/utils/appPaths.d.ts.map +1 -0
  165. package/dist/utils/appPaths.js +162 -0
  166. package/dist/utils/appPaths.js.map +1 -0
  167. package/dist/utils/fs-utils.d.ts +50 -0
  168. package/dist/utils/fs-utils.d.ts.map +1 -0
  169. package/dist/utils/fs-utils.js +114 -0
  170. package/dist/utils/fs-utils.js.map +1 -0
  171. package/dist/utils/index.d.ts +12 -0
  172. package/dist/utils/index.d.ts.map +1 -0
  173. package/dist/utils/index.js +10 -0
  174. package/dist/utils/index.js.map +1 -0
  175. package/dist/utils/standardConfig.d.ts +61 -0
  176. package/dist/utils/standardConfig.d.ts.map +1 -0
  177. package/dist/utils/standardConfig.js +109 -0
  178. package/dist/utils/standardConfig.js.map +1 -0
  179. package/dist/utils/startupBanner.d.ts +34 -0
  180. package/dist/utils/startupBanner.d.ts.map +1 -0
  181. package/dist/utils/startupBanner.js +169 -0
  182. package/dist/utils/startupBanner.js.map +1 -0
  183. package/dist/utils/startupLogger.d.ts +45 -0
  184. package/dist/utils/startupLogger.d.ts.map +1 -0
  185. package/dist/utils/startupLogger.js +200 -0
  186. package/dist/utils/startupLogger.js.map +1 -0
  187. package/package.json +151 -0
  188. package/src/api/logsRouter.ts +600 -0
  189. package/src/cli/dev-server.ts +803 -0
  190. package/src/cli/index.ts +31 -0
  191. package/src/core/StandardServer.ts +587 -0
  192. package/src/core/apiResponse.ts +202 -0
  193. package/src/core/healthCheck.ts +565 -0
  194. package/src/core/index.ts +80 -0
  195. package/src/core/logger.ts +1092 -0
  196. package/src/core/portUtils.ts +319 -0
  197. package/src/core/storageService.ts +595 -0
  198. package/src/desktop/bundler.ts +271 -0
  199. package/src/desktop/index.ts +18 -0
  200. package/src/desktop/native-modules.ts +289 -0
  201. package/src/index.ts +142 -0
  202. package/src/logging/LogCategories.ts +302 -0
  203. package/src/middleware/aiErrorHandler.ts +278 -0
  204. package/src/middleware/auth.ts +329 -0
  205. package/src/middleware/cors.ts +187 -0
  206. package/src/middleware/errorHandler.ts +103 -0
  207. package/src/middleware/fileUpload.ts +252 -0
  208. package/src/middleware/health.ts +206 -0
  209. package/src/middleware/index.ts +71 -0
  210. package/src/middleware/openapi.ts +305 -0
  211. package/src/middleware/requestLogging.ts +92 -0
  212. package/src/middleware/session.ts +238 -0
  213. package/src/middleware/validation.ts +603 -0
  214. package/src/services/aiService.ts +789 -0
  215. package/src/services/conversationStorage.ts +232 -0
  216. package/src/services/crossPlatformBuffer.ts +341 -0
  217. package/src/services/index.ts +47 -0
  218. package/src/services/networkService.ts +351 -0
  219. package/src/services/queueService.ts +446 -0
  220. package/src/services/settingsService.ts +549 -0
  221. package/src/services/systemMonitor.ts +936 -0
  222. package/src/services/updateService.ts +334 -0
  223. package/src/services/websocketEvents.ts +409 -0
  224. package/src/services/websocketServer.ts +394 -0
  225. package/src/settings/SettingsSchema.ts +664 -0
  226. package/src/testing/TestServer.ts +312 -0
  227. package/src/types/index.ts +154 -0
  228. package/src/utils/appPaths.ts +196 -0
  229. package/src/utils/fs-utils.ts +130 -0
  230. package/src/utils/index.ts +15 -0
  231. package/src/utils/standardConfig.ts +178 -0
  232. package/src/utils/startupBanner.ts +287 -0
  233. package/src/utils/startupLogger.ts +268 -0
  234. package/ui/dist/index.d.mts +1221 -0
  235. package/ui/dist/index.d.ts +1221 -0
  236. package/ui/dist/index.js +73 -0
  237. package/ui/dist/index.js.map +1 -0
  238. package/ui/dist/index.mjs +73 -0
  239. package/ui/dist/index.mjs.map +1 -0
@@ -0,0 +1,789 @@
1
+ /**
2
+ * AI Service Interface for multiple AI providers
3
+ * Provides a unified interface for OpenAI, Claude, and other AI services
4
+ */
5
+
6
+ import { EventEmitter } from "events";
7
+ import { createLogger } from "../core/index.js";
8
+ import { getStorageService } from "../core/storageService.js";
9
+ import OpenAI from "openai";
10
+ import * as crypto from "node:crypto";
11
+
12
+ /**
13
+ * Custom AI Error class that preserves structured error information
14
+ */
15
+ export class AIError extends Error {
16
+ public statusCode: number;
17
+ public errorType: string;
18
+ public provider: string;
19
+
20
+ constructor(
21
+ message: string,
22
+ statusCode: number = 500,
23
+ errorType: string = "AI_ERROR",
24
+ provider: string = "unknown",
25
+ ) {
26
+ super(message);
27
+ this.name = "AIError";
28
+ this.statusCode = statusCode;
29
+ this.errorType = errorType;
30
+ this.provider = provider;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Simple LRU Cache implementation to prevent memory leaks
36
+ */
37
+ class LRUCache<K, V> {
38
+ private cache = new Map<K, V>();
39
+ private maxSize: number;
40
+
41
+ constructor(maxSize: number = 1000) {
42
+ this.maxSize = maxSize;
43
+ }
44
+
45
+ get(key: K): V | undefined {
46
+ if (this.cache.has(key)) {
47
+ // Move to end (most recent)
48
+ const value = this.cache.get(key);
49
+ this.cache.delete(key);
50
+ this.cache.set(key, value!);
51
+ return value;
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ set(key: K, value: V): void {
57
+ if (this.cache.has(key)) {
58
+ // Update existing
59
+ this.cache.delete(key);
60
+ } else if (this.cache.size >= this.maxSize) {
61
+ // Remove oldest (first entry)
62
+ const firstKey = this.cache.keys().next().value;
63
+ if (firstKey !== undefined) {
64
+ this.cache.delete(firstKey);
65
+ }
66
+ }
67
+ this.cache.set(key, value);
68
+ }
69
+
70
+ has(key: K): boolean {
71
+ return this.cache.has(key);
72
+ }
73
+
74
+ clear(): void {
75
+ this.cache.clear();
76
+ }
77
+
78
+ get size(): number {
79
+ return this.cache.size;
80
+ }
81
+ }
82
+
83
+ let logger: any; // Will be initialized when needed
84
+
85
+ function ensureLogger() {
86
+ if (!logger) {
87
+ logger = createLogger("AIService");
88
+ }
89
+ return logger;
90
+ }
91
+
92
+ export interface AIConfig {
93
+ provider: "openai" | "anthropic" | "mock";
94
+ apiKey?: string;
95
+ model?: string;
96
+ temperature?: number;
97
+ maxTokens?: number;
98
+ baseURL?: string;
99
+ enableCaching?: boolean;
100
+ enableLogging?: boolean;
101
+ models?: {
102
+ chat?: string;
103
+ template?: string;
104
+ fileAnalysis?: string;
105
+ validation?: string;
106
+ };
107
+ }
108
+
109
+ export interface AIMessage {
110
+ role: "system" | "user" | "assistant";
111
+ content: string;
112
+ }
113
+
114
+ export interface AIResponse {
115
+ content: string;
116
+ model: string;
117
+ usage?: {
118
+ promptTokens: number;
119
+ completionTokens: number;
120
+ totalTokens: number;
121
+ };
122
+ cost?: number;
123
+ cached?: boolean;
124
+ }
125
+
126
+ export interface TemplateResponse extends AIResponse {
127
+ template?: any;
128
+ }
129
+
130
+ export interface FileAnalysisResponse extends AIResponse {
131
+ analysis?: string;
132
+ dataPoints?: any[];
133
+ }
134
+
135
+ export interface AIAnalysisOptions {
136
+ analysisType?: string;
137
+ requiresStructuredOutput?: boolean;
138
+ requiresReasoning?: boolean;
139
+ temperature?: number;
140
+ maxTokens?: number;
141
+ systemPrompt?: string;
142
+ responseFormat?: { type: string };
143
+ }
144
+
145
+ /**
146
+ * Abstract base class for AI providers
147
+ */
148
+ export abstract class AIProvider {
149
+ protected config: AIConfig;
150
+
151
+ constructor(config: AIConfig) {
152
+ this.config = config;
153
+ }
154
+
155
+ abstract analyze(
156
+ prompt: string,
157
+ options?: AIAnalysisOptions,
158
+ ): Promise<AIResponse>;
159
+ abstract chat(
160
+ messages: AIMessage[],
161
+ options?: AIAnalysisOptions,
162
+ ): Promise<AIResponse>;
163
+ abstract validateConfig(): boolean;
164
+ }
165
+
166
+ /**
167
+ * Model cost configuration
168
+ */
169
+ const MODEL_COSTS: Record<string, { input: number; output: number }> = {
170
+ "gpt-3.5-turbo": { input: 0.0005, output: 0.0015 },
171
+ "gpt-4": { input: 0.01, output: 0.03 },
172
+ "gpt-4-turbo": { input: 0.01, output: 0.03 },
173
+ "gpt-4-turbo-preview": { input: 0.01, output: 0.03 },
174
+ "gpt-4o": { input: 0.005, output: 0.015 },
175
+ "gpt-4o-mini": { input: 0.00015, output: 0.0006 },
176
+ };
177
+
178
+ /**
179
+ * OpenAI Provider Implementation
180
+ */
181
+ export class OpenAIProvider extends AIProvider {
182
+ private client: OpenAI | null = null;
183
+ private models: {
184
+ chat: string;
185
+ template: string;
186
+ fileAnalysis: string;
187
+ validation: string;
188
+ };
189
+
190
+ constructor(config: AIConfig) {
191
+ super(config);
192
+
193
+ // Set default models or use provided ones
194
+ this.models = {
195
+ chat: config.models?.chat || config.model || "gpt-3.5-turbo",
196
+ template: config.models?.template || "gpt-4-turbo-preview",
197
+ fileAnalysis: config.models?.fileAnalysis || "gpt-4-turbo",
198
+ validation: config.models?.validation || "gpt-3.5-turbo",
199
+ };
200
+
201
+ if (config.apiKey) {
202
+ this.initializeClient();
203
+ }
204
+ }
205
+
206
+ private initializeClient(): void {
207
+ if (this.client) return;
208
+
209
+ try {
210
+ const apiKey = this.config.apiKey || process.env.OPENAI_API_KEY;
211
+
212
+ if (!apiKey) {
213
+ throw new AIError(
214
+ "OpenAI API key not configured. Set OPENAI_API_KEY environment variable or provide apiKey in config.",
215
+ 401,
216
+ "AUTH_ERROR",
217
+ "openai",
218
+ );
219
+ }
220
+
221
+ // Basic validation - OpenAI keys start with 'sk-'
222
+ if (!apiKey.startsWith("sk-")) {
223
+ throw new AIError(
224
+ "Invalid OpenAI API key format",
225
+ 401,
226
+ "AUTH_ERROR",
227
+ "openai",
228
+ );
229
+ }
230
+
231
+ this.client = new OpenAI({
232
+ apiKey,
233
+ baseURL: this.config.baseURL,
234
+ });
235
+ ensureLogger().info("OpenAI client initialized");
236
+ } catch (_error) {
237
+ ensureLogger().error("Failed to initialize OpenAI client:", _error);
238
+ if (_error instanceof AIError) {
239
+ throw _error;
240
+ }
241
+ throw new AIError(
242
+ "Failed to initialize OpenAI client",
243
+ 500,
244
+ "INIT_ERROR",
245
+ "openai",
246
+ );
247
+ }
248
+ }
249
+
250
+ validateConfig(): boolean {
251
+ return !!this.config.apiKey;
252
+ }
253
+
254
+ async analyze(
255
+ prompt: string,
256
+ options: AIAnalysisOptions = {},
257
+ ): Promise<AIResponse> {
258
+ const messages: AIMessage[] = [];
259
+
260
+ if (options.systemPrompt) {
261
+ messages.push({ role: "system", content: options.systemPrompt });
262
+ }
263
+
264
+ messages.push({ role: "user", content: prompt });
265
+
266
+ return this.chat(messages, options);
267
+ }
268
+
269
+ async chat(
270
+ messages: AIMessage[],
271
+ options: AIAnalysisOptions = {},
272
+ ): Promise<AIResponse> {
273
+ try {
274
+ if (!this.client) {
275
+ this.initializeClient();
276
+ }
277
+
278
+ const model =
279
+ options.analysisType === "template"
280
+ ? this.models.template
281
+ : options.analysisType === "fileAnalysis"
282
+ ? this.models.fileAnalysis
283
+ : options.analysisType === "validation"
284
+ ? this.models.validation
285
+ : this.models.chat;
286
+
287
+ ensureLogger().debug("Sending request to OpenAI", {
288
+ model,
289
+ messageCount: messages.length,
290
+ });
291
+
292
+ const completion = await this.client!.chat.completions.create({
293
+ model,
294
+ messages: messages as any,
295
+ temperature: options.temperature ?? this.config.temperature ?? 0.7,
296
+ max_tokens: options.maxTokens ?? this.config.maxTokens ?? 2000,
297
+ ...(options.responseFormat && {
298
+ response_format: options.responseFormat as any,
299
+ }),
300
+ });
301
+
302
+ const content = completion.choices[0].message.content || "";
303
+ const cost = this.calculateCost(completion.usage, model);
304
+
305
+ const response: AIResponse = {
306
+ content,
307
+ model,
308
+ usage: completion.usage
309
+ ? {
310
+ promptTokens: completion.usage.prompt_tokens,
311
+ completionTokens: completion.usage.completion_tokens,
312
+ totalTokens: completion.usage.total_tokens,
313
+ }
314
+ : undefined,
315
+ cost,
316
+ };
317
+
318
+ if (this.config.enableLogging) {
319
+ ensureLogger().info("OpenAI request completed", {
320
+ model,
321
+ tokens: completion.usage?.total_tokens,
322
+ cost,
323
+ });
324
+ }
325
+
326
+ return response;
327
+ } catch (_error: any) {
328
+ ensureLogger().error("OpenAI API error", _error);
329
+ throw this.handleOpenAIError(_error);
330
+ }
331
+ }
332
+
333
+ private calculateCost(usage: any, model: string): number {
334
+ if (!usage) return 0;
335
+
336
+ const costs = MODEL_COSTS[model] || MODEL_COSTS["gpt-3.5-turbo"];
337
+ const inputCost = (usage.prompt_tokens / 1000) * costs.input;
338
+ const outputCost = (usage.completion_tokens / 1000) * costs.output;
339
+
340
+ return Math.round((inputCost + outputCost) * 1000000) / 1000000;
341
+ }
342
+
343
+ private handleOpenAIError(error: any): AIError {
344
+ if (error.response?.status === 401) {
345
+ return new AIError("Invalid API key", 401, "AUTH_ERROR", "openai");
346
+ }
347
+ if (error.response?.status === 429) {
348
+ return new AIError(
349
+ "Rate limit exceeded. Please try again later.",
350
+ 429,
351
+ "RATE_LIMIT",
352
+ "openai",
353
+ );
354
+ }
355
+ if (error.response?.status === 503) {
356
+ return new AIError(
357
+ "OpenAI service temporarily unavailable",
358
+ 503,
359
+ "SERVICE_UNAVAILABLE",
360
+ "openai",
361
+ );
362
+ }
363
+ if (error.message?.includes("timeout")) {
364
+ return new AIError(
365
+ "Request timed out. Please try again.",
366
+ 408,
367
+ "TIMEOUT",
368
+ "openai",
369
+ );
370
+ }
371
+ // Preserve original error info if not a known error type
372
+ const statusCode = error.response?.status || error.statusCode || 500;
373
+ const errorType = error.type || "UNKNOWN_ERROR";
374
+ return new AIError(
375
+ error.message || "Unknown error occurred",
376
+ statusCode,
377
+ errorType,
378
+ "openai",
379
+ );
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Mock Provider for testing
385
+ */
386
+ export class MockAIProvider extends AIProvider {
387
+ validateConfig(): boolean {
388
+ return true;
389
+ }
390
+
391
+ async analyze(
392
+ prompt: string,
393
+ _options: AIAnalysisOptions = {},
394
+ ): Promise<AIResponse> {
395
+ return {
396
+ content: `Mock analysis for: ${prompt.substring(0, 50)}...`,
397
+ model: "mock",
398
+ usage: {
399
+ promptTokens: 10,
400
+ completionTokens: 5,
401
+ totalTokens: 15,
402
+ },
403
+ cost: 0,
404
+ };
405
+ }
406
+
407
+ async chat(
408
+ messages: AIMessage[],
409
+ _options: AIAnalysisOptions = {},
410
+ ): Promise<AIResponse> {
411
+ return {
412
+ content: `Mock chat response for ${messages.length} messages`,
413
+ model: "mock",
414
+ usage: {
415
+ promptTokens: 10,
416
+ completionTokens: 5,
417
+ totalTokens: 15,
418
+ },
419
+ cost: 0,
420
+ };
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Main AI Service with caching and model routing
426
+ */
427
+ export class AIService extends EventEmitter {
428
+ private providers: Map<string, AIProvider>;
429
+ private cache: LRUCache<string, AIResponse>;
430
+ private fileHandler = getStorageService();
431
+ private defaultProvider: string;
432
+
433
+ constructor() {
434
+ super();
435
+ this.providers = new Map();
436
+ this.cache = new LRUCache(1000); // Limit cache to 1000 entries to prevent memory leaks
437
+ this.defaultProvider = "mock";
438
+
439
+ ensureLogger().debug("AIService initialized");
440
+ }
441
+
442
+ /**
443
+ * Register an AI provider
444
+ */
445
+ registerProvider(name: string, config: AIConfig): void {
446
+ let provider: AIProvider;
447
+
448
+ switch (config.provider) {
449
+ case "openai":
450
+ provider = new OpenAIProvider(config);
451
+ break;
452
+ case "mock":
453
+ provider = new MockAIProvider(config);
454
+ break;
455
+ default:
456
+ throw new Error(`Unknown provider: ${config.provider}`);
457
+ }
458
+
459
+ if (!provider.validateConfig()) {
460
+ throw new Error(`Invalid configuration for provider: ${name}`);
461
+ }
462
+
463
+ this.providers.set(name, provider);
464
+
465
+ if (!this.defaultProvider || this.providers.size === 1) {
466
+ this.defaultProvider = name;
467
+ }
468
+
469
+ ensureLogger().info(`Registered AI provider: ${name} (${config.provider})`);
470
+ }
471
+
472
+ /**
473
+ * Set default provider
474
+ */
475
+ setDefaultProvider(name: string): void {
476
+ if (!this.providers.has(name)) {
477
+ throw new Error(`Provider not found: ${name}`);
478
+ }
479
+ this.defaultProvider = name;
480
+ ensureLogger().info(`Set default provider to: ${name}`);
481
+ }
482
+
483
+ /**
484
+ * Analyze content using AI
485
+ */
486
+ async analyze(
487
+ prompt: string,
488
+ options: AIAnalysisOptions & { provider?: string; useCache?: boolean } = {},
489
+ ): Promise<AIResponse> {
490
+ const providerName = options.provider || this.defaultProvider;
491
+ const provider = this.providers.get(providerName);
492
+
493
+ if (!provider) {
494
+ throw new Error(`Provider not found: ${providerName}`);
495
+ }
496
+
497
+ // Check cache if enabled
498
+ const cacheKey = this.getCacheKey(prompt, options);
499
+ if (options.useCache !== false && this.cache.has(cacheKey)) {
500
+ const cached = this.cache.get(cacheKey)!;
501
+ ensureLogger().debug("Returning cached response", { cacheKey });
502
+ return { ...cached, cached: true };
503
+ }
504
+
505
+ // Log the analysis request
506
+ this.emit("analysis:start", { prompt, options, provider: providerName });
507
+
508
+ try {
509
+ const response = await provider.analyze(prompt, options);
510
+
511
+ // Cache the response
512
+ this.cache.set(cacheKey, response);
513
+
514
+ // Log usage for tracking
515
+ if (response.usage) {
516
+ await this.logUsage(providerName, response);
517
+ }
518
+
519
+ this.emit("analysis:complete", { response, provider: providerName });
520
+
521
+ return response;
522
+ } catch (_error) {
523
+ this.emit("analysis:error", { error: _error, provider: providerName });
524
+ throw _error;
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Chat with AI
530
+ */
531
+ async chat(
532
+ messages: AIMessage[],
533
+ options: AIAnalysisOptions & { provider?: string; useCache?: boolean } = {},
534
+ ): Promise<AIResponse> {
535
+ const providerName = options.provider || this.defaultProvider;
536
+ const provider = this.providers.get(providerName);
537
+
538
+ if (!provider) {
539
+ throw new Error(`Provider not found: ${providerName}`);
540
+ }
541
+
542
+ // Check cache if enabled
543
+ const cacheKey = this.getCacheKey(JSON.stringify(messages), options);
544
+ if (options.useCache !== false && this.cache.has(cacheKey)) {
545
+ const cached = this.cache.get(cacheKey)!;
546
+ ensureLogger().debug("Returning cached response", { cacheKey });
547
+ return { ...cached, cached: true };
548
+ }
549
+
550
+ this.emit("chat:start", { messages, options, provider: providerName });
551
+
552
+ try {
553
+ const response = await provider.chat(messages, options);
554
+
555
+ // Cache the response
556
+ this.cache.set(cacheKey, response);
557
+
558
+ // Log usage for tracking
559
+ if (response.usage) {
560
+ await this.logUsage(providerName, response);
561
+ }
562
+
563
+ this.emit("chat:complete", { response, provider: providerName });
564
+
565
+ return response;
566
+ } catch (_error) {
567
+ this.emit("chat:error", { error: _error, provider: providerName });
568
+ throw _error;
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Generate template from conversation
574
+ */
575
+ async generateTemplate(
576
+ messages: AIMessage[],
577
+ options: AIAnalysisOptions & { provider?: string } = {},
578
+ ): Promise<TemplateResponse> {
579
+ const providerName = options.provider || this.defaultProvider;
580
+
581
+ // Add template generation request
582
+ const generationRequest: AIMessage = {
583
+ role: "user",
584
+ content:
585
+ "Based on our conversation, generate a template. Return ONLY the JSON template, no explanation.",
586
+ };
587
+
588
+ const allMessages = [...messages, generationRequest];
589
+
590
+ // Use structured output for template generation
591
+ const response = await this.chat(allMessages, {
592
+ ...options,
593
+ analysisType: "template",
594
+ temperature: options.temperature ?? 0.2,
595
+ maxTokens: options.maxTokens ?? 4000,
596
+ responseFormat: { type: "json_object" },
597
+ provider: providerName,
598
+ });
599
+
600
+ // Parse and validate the template
601
+ let template;
602
+ try {
603
+ template = JSON.parse(response.content);
604
+ } catch (_e) {
605
+ throw new Error("Generated template is not valid JSON");
606
+ }
607
+
608
+ return {
609
+ ...response,
610
+ template,
611
+ } as TemplateResponse;
612
+ }
613
+
614
+ /**
615
+ * Analyze uploaded file
616
+ */
617
+ async analyzeFile(
618
+ fileContent: string,
619
+ fileType: string,
620
+ deviceInfo: Record<string, any> = {},
621
+ options: AIAnalysisOptions & { provider?: string } = {},
622
+ ): Promise<FileAnalysisResponse> {
623
+ const messages: AIMessage[] = [
624
+ {
625
+ role: "system",
626
+ content:
627
+ options.systemPrompt ||
628
+ "You are an expert at analyzing device documentation and extracting data point information.",
629
+ },
630
+ {
631
+ role: "user",
632
+ content: `Analyze this ${fileType} file for device "${deviceInfo.name || "Unknown"}" by ${deviceInfo.manufacturer || "Unknown"}:\n\n${fileContent}\n\nExtract all data points, registers, and configuration information.`,
633
+ },
634
+ ];
635
+
636
+ const response = await this.chat(messages, {
637
+ ...options,
638
+ analysisType: "fileAnalysis",
639
+ temperature: options.temperature ?? 0.3,
640
+ maxTokens: options.maxTokens ?? 4000,
641
+ provider: options.provider,
642
+ });
643
+
644
+ // Try to extract data points from the response
645
+ let dataPoints: any[] = [];
646
+ try {
647
+ // Attempt to parse JSON data points from the response
648
+ const jsonMatch = response.content.match(/\[[\s\S]*\]/);
649
+ if (jsonMatch) {
650
+ dataPoints = JSON.parse(jsonMatch[0]);
651
+ }
652
+ } catch (_e) {
653
+ // If parsing fails, return empty array
654
+ ensureLogger().debug("Could not parse data points from file analysis");
655
+ }
656
+
657
+ return {
658
+ ...response,
659
+ analysis: response.content,
660
+ dataPoints,
661
+ } as FileAnalysisResponse;
662
+ }
663
+
664
+ /**
665
+ * Select best model for task
666
+ */
667
+ selectModel(criteria: {
668
+ contentLength?: number;
669
+ complexity?: "low" | "medium" | "high";
670
+ requiresReasoning?: boolean;
671
+ requiresStructuredOutput?: boolean;
672
+ costSensitive?: boolean;
673
+ }): { provider: string; model: string; reason: string } {
674
+ // Simple model selection logic - can be enhanced
675
+ if (criteria.costSensitive) {
676
+ return {
677
+ provider: "openai",
678
+ model: "gpt-3.5-turbo",
679
+ reason: "Cost-optimized model",
680
+ };
681
+ }
682
+
683
+ if (criteria.requiresReasoning || criteria.complexity === "high") {
684
+ return {
685
+ provider: "openai",
686
+ model: "gpt-4",
687
+ reason: "Advanced reasoning required",
688
+ };
689
+ }
690
+
691
+ if (criteria.contentLength && criteria.contentLength > 8000) {
692
+ return {
693
+ provider: "anthropic",
694
+ model: "claude-3-opus",
695
+ reason: "Long context window needed",
696
+ };
697
+ }
698
+
699
+ return {
700
+ provider: "openai",
701
+ model: "gpt-4-turbo",
702
+ reason: "Default balanced model",
703
+ };
704
+ }
705
+
706
+ /**
707
+ * Generate cache key
708
+ */
709
+ private getCacheKey(content: string, options: any): string {
710
+ const hash = crypto
711
+ .createHash("sha256")
712
+ .update(content + JSON.stringify(options))
713
+ .digest("hex");
714
+ return hash.substring(0, 16);
715
+ }
716
+
717
+ /**
718
+ * Log AI usage for tracking
719
+ */
720
+ private async logUsage(
721
+ provider: string,
722
+ response: AIResponse,
723
+ ): Promise<void> {
724
+ const usage = {
725
+ provider,
726
+ model: response.model,
727
+ timestamp: new Date().toISOString(),
728
+ tokens: response.usage,
729
+ cost: response.cost,
730
+ };
731
+
732
+ try {
733
+ await this.fileHandler.saveFile(
734
+ `ai_usage_${Date.now()}.json`,
735
+ JSON.stringify(usage, null, 2),
736
+ "logs",
737
+ { overwrite: false },
738
+ );
739
+ } catch (_error) {
740
+ ensureLogger().error("Failed to log AI usage", _error);
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Clear cache
746
+ */
747
+ clearCache(): void {
748
+ this.cache.clear();
749
+ ensureLogger().info("AI response cache cleared");
750
+ }
751
+
752
+ /**
753
+ * Get usage statistics
754
+ */
755
+ async getUsageStats(
756
+ _startDate?: Date,
757
+ _endDate?: Date,
758
+ ): Promise<{
759
+ totalTokens: number;
760
+ totalCost: number;
761
+ byProvider: Record<string, { tokens: number; cost: number }>;
762
+ }> {
763
+ // Implementation would read from usage logs
764
+ return {
765
+ totalTokens: 0,
766
+ totalCost: 0,
767
+ byProvider: {},
768
+ };
769
+ }
770
+ }
771
+
772
+ // Export singleton instance with lazy initialization
773
+ let _aiService: AIService | null = null;
774
+
775
+ export function getAIService(): AIService {
776
+ if (!_aiService) {
777
+ _aiService = new AIService();
778
+ }
779
+ return _aiService;
780
+ }
781
+
782
+ // For backward compatibility - use a Proxy to lazy-load
783
+ export const aiService = new Proxy({} as AIService, {
784
+ get(_target, prop) {
785
+ return (getAIService() as any)[prop];
786
+ },
787
+ });
788
+
789
+ export default AIService;