@smythos/sre 1.6.14 → 1.7.5

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 (115) hide show
  1. package/CHANGELOG +15 -0
  2. package/dist/index.js +66 -58
  3. package/dist/index.js.map +1 -1
  4. package/dist/types/Components/APIEndpoint.class.d.ts +2 -8
  5. package/dist/types/Components/Component.class.d.ts +9 -0
  6. package/dist/types/Components/Triggers/Gmail.trigger.d.ts +0 -17
  7. package/dist/types/Components/Triggers/JobScheduler.trigger.d.ts +10 -0
  8. package/dist/types/Components/Triggers/Trigger.class.d.ts +11 -0
  9. package/dist/types/Components/index.d.ts +6 -0
  10. package/dist/types/Core/Connector.class.d.ts +1 -0
  11. package/dist/types/Core/ConnectorsService.d.ts +2 -0
  12. package/dist/types/Core/HookService.d.ts +1 -1
  13. package/dist/types/helpers/BinaryInput.helper.d.ts +1 -1
  14. package/dist/types/helpers/Conversation.helper.d.ts +2 -0
  15. package/dist/types/helpers/Crypto.helper.d.ts +8 -0
  16. package/dist/types/helpers/LocalCache.helper.d.ts +18 -0
  17. package/dist/types/helpers/TemplateString.helper.d.ts +2 -1
  18. package/dist/types/index.d.ts +13 -0
  19. package/dist/types/subsystems/AgentManager/Agent.class.d.ts +4 -2
  20. package/dist/types/subsystems/AgentManager/AgentData.service/AgentDataConnector.d.ts +13 -0
  21. package/dist/types/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.d.ts +1 -4
  22. package/dist/types/subsystems/AgentManager/Scheduler.service/Job.class.d.ts +29 -6
  23. package/dist/types/subsystems/AgentManager/Scheduler.service/SchedulerConnector.d.ts +11 -3
  24. package/dist/types/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.d.ts +31 -7
  25. package/dist/types/subsystems/IO/VectorDB.service/VectorDBConnector.d.ts +4 -4
  26. package/dist/types/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.d.ts +2 -2
  27. package/dist/types/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.d.ts +2 -2
  28. package/dist/types/subsystems/IO/VectorDB.service/connectors/RAMVecrtorDB.class.d.ts +2 -2
  29. package/dist/types/subsystems/IO/VectorDB.service/embed/BaseEmbedding.d.ts +16 -9
  30. package/dist/types/subsystems/IO/VectorDB.service/embed/index.d.ts +4 -1
  31. package/dist/types/subsystems/LLMManager/LLM.inference.d.ts +36 -2
  32. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +2 -5
  33. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +3 -6
  34. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +7 -0
  35. package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +2 -5
  36. package/dist/types/types/Agent.types.d.ts +1 -0
  37. package/dist/types/types/LLM.types.d.ts +56 -36
  38. package/dist/types/types/SRE.types.d.ts +4 -1
  39. package/dist/types/types/VectorDB.types.d.ts +6 -3
  40. package/dist/types/utils/string.utils.d.ts +0 -4
  41. package/package.json +6 -2
  42. package/src/Components/APICall/OAuth.helper.ts +30 -35
  43. package/src/Components/APIEndpoint.class.ts +25 -6
  44. package/src/Components/Classifier.class.ts +8 -2
  45. package/src/Components/Component.class.ts +11 -0
  46. package/src/Components/GenAILLM.class.ts +11 -7
  47. package/src/Components/LLMAssistant.class.ts +12 -3
  48. package/src/Components/ScrapflyWebScrape.class.ts +8 -1
  49. package/src/Components/TavilyWebSearch.class.ts +4 -1
  50. package/src/Components/Triggers/Gmail.trigger.ts +282 -0
  51. package/src/Components/Triggers/JobScheduler.trigger.ts +45 -0
  52. package/src/Components/Triggers/README.md +3 -0
  53. package/src/Components/Triggers/Trigger.class.ts +101 -0
  54. package/src/Components/Triggers/WhatsApp.trigger.ts +219 -0
  55. package/src/Components/index.ts +8 -0
  56. package/src/Core/AgentProcess.helper.ts +4 -6
  57. package/src/Core/Connector.class.ts +11 -3
  58. package/src/Core/ConnectorsService.ts +5 -0
  59. package/src/Core/ExternalEventsReceiver.ts +317 -0
  60. package/src/Core/HookService.ts +20 -6
  61. package/src/Core/SmythRuntime.class.ts +20 -2
  62. package/src/Core/SystemEvents.ts +17 -0
  63. package/src/Core/boot.ts +2 -0
  64. package/src/helpers/BinaryInput.helper.ts +8 -8
  65. package/src/helpers/Conversation.helper.ts +46 -12
  66. package/src/helpers/Crypto.helper.ts +28 -0
  67. package/src/helpers/LocalCache.helper.ts +18 -0
  68. package/src/helpers/TemplateString.helper.ts +20 -9
  69. package/src/index.ts +13 -0
  70. package/src/index.ts.bak +13 -0
  71. package/src/subsystems/AGENTS.md +594 -0
  72. package/src/subsystems/AgentManager/Agent.class.ts +73 -21
  73. package/src/subsystems/AgentManager/AgentData.service/AgentDataConnector.ts +30 -6
  74. package/src/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.ts +2 -2
  75. package/src/subsystems/AgentManager/AgentLogger.class.ts +1 -1
  76. package/src/subsystems/AgentManager/AgentRuntime.class.ts +34 -5
  77. package/src/subsystems/AgentManager/Scheduler.service/Job.class.ts +414 -0
  78. package/src/subsystems/AgentManager/Scheduler.service/Schedule.class.ts +200 -0
  79. package/src/subsystems/AgentManager/Scheduler.service/SchedulerConnector.ts +200 -0
  80. package/src/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.ts +767 -0
  81. package/src/subsystems/AgentManager/Scheduler.service/index.ts +11 -0
  82. package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +15 -4
  83. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +32 -11
  84. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +27 -10
  85. package/src/subsystems/IO/VectorDB.service/connectors/RAMVecrtorDB.class.ts +25 -9
  86. package/src/subsystems/IO/VectorDB.service/embed/BaseEmbedding.ts +182 -12
  87. package/src/subsystems/IO/VectorDB.service/embed/GoogleEmbedding.ts +1 -1
  88. package/src/subsystems/IO/VectorDB.service/embed/OpenAIEmbedding.ts +1 -1
  89. package/src/subsystems/IO/VectorDB.service/embed/index.ts +12 -2
  90. package/src/subsystems/LLMManager/LLM.inference.ts +76 -17
  91. package/src/subsystems/LLMManager/LLM.service/LLMCredentials.helper.ts +61 -2
  92. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +3 -0
  93. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +3 -1
  94. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +5 -1
  95. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +247 -56
  96. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +3 -0
  97. package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +28 -21
  98. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +3 -0
  99. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +121 -33
  100. package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +38 -27
  101. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ChatCompletionsApiInterface.ts +3 -2
  102. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +117 -20
  103. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +3 -0
  104. package/src/subsystems/LLMManager/ModelsProvider.service/ModelsProviderConnector.ts +3 -8
  105. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/JSONModelsProvider.class.ts +4 -1
  106. package/src/subsystems/MemoryManager/Cache.service/connectors/RedisCache.class.ts +12 -0
  107. package/src/subsystems/MemoryManager/LLMContext.ts +3 -8
  108. package/src/subsystems/MemoryManager/RuntimeContext.ts +10 -9
  109. package/src/subsystems/Security/Credentials/Credentials.class.ts +1 -0
  110. package/src/subsystems/Security/Credentials/ManagedOAuth2Credentials.class.ts +106 -0
  111. package/src/types/Agent.types.ts +1 -0
  112. package/src/types/LLM.types.ts +68 -40
  113. package/src/types/SRE.types.ts +3 -0
  114. package/src/types/VectorDB.types.ts +7 -3
  115. package/src/utils/string.utils.ts +193 -191
@@ -24,6 +24,7 @@ import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.cla
24
24
  import { LLMConnector } from '../LLMConnector';
25
25
  import { SystemEvents } from '@sre/Core/SystemEvents';
26
26
  import { Logger } from '@sre/helpers/Log.helper';
27
+ import { hookAsync } from '@sre/Core/HookService';
27
28
 
28
29
  const logger = Logger('VertexAIConnector');
29
30
 
@@ -49,6 +50,7 @@ export class VertexAIConnector extends LLMConnector {
49
50
  });
50
51
  }
51
52
 
53
+ @hookAsync('LLMConnector.request')
52
54
  protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
53
55
  try {
54
56
  logger.debug(`request ${this.name}`, acRequest.candidate);
@@ -110,6 +112,7 @@ export class VertexAIConnector extends LLMConnector {
110
112
  }
111
113
  }
112
114
 
115
+ @hookAsync('LLMConnector.streamRequest')
113
116
  protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
114
117
  const emitter = new EventEmitter();
115
118
 
@@ -380,51 +383,136 @@ export class VertexAIConnector extends LLMConnector {
380
383
  }): TLLMToolResultMessageBlock[] {
381
384
  const messageBlocks: TLLMToolResultMessageBlock[] = [];
382
385
 
386
+ const parseFunctionArgs = (args: unknown) => {
387
+ if (typeof args === 'string') {
388
+ try {
389
+ return JSON.parse(args);
390
+ } catch {
391
+ return args;
392
+ }
393
+ }
394
+ return args ?? {};
395
+ };
396
+
397
+ const parseFunctionResponse = (response: unknown): any => {
398
+ if (typeof response === 'string') {
399
+ try {
400
+ const parsed = JSON.parse(response);
401
+ if (typeof parsed === 'string' && parsed !== response) {
402
+ return parseFunctionResponse(parsed);
403
+ }
404
+ return parsed;
405
+ } catch {
406
+ return response;
407
+ }
408
+ }
409
+ return response ?? {};
410
+ };
411
+
383
412
  if (messageBlock) {
384
- const parts = [];
413
+ const parts: any[] = [];
385
414
 
386
- if (typeof messageBlock.content === 'string') {
387
- parts.push({ text: messageBlock.content });
388
- } else if (Array.isArray(messageBlock.content)) {
389
- parts.push(...messageBlock.content);
415
+ if (Array.isArray(messageBlock.parts) && messageBlock.parts.length > 0) {
416
+ for (const part of messageBlock.parts) {
417
+ if (!part) continue;
418
+
419
+ if (typeof part.text === 'string' && part.text.trim()) {
420
+ parts.push({ text: part.text.trim() });
421
+ continue;
422
+ }
423
+
424
+ if (part.functionCall) {
425
+ parts.push({
426
+ functionCall: {
427
+ name: part.functionCall.name,
428
+ args: parseFunctionArgs(part.functionCall.args),
429
+ },
430
+ });
431
+ continue;
432
+ }
433
+
434
+ if (part.functionResponse) {
435
+ parts.push({
436
+ functionResponse: {
437
+ name: part.functionResponse.name,
438
+ response: parseFunctionResponse(part.functionResponse.response),
439
+ },
440
+ });
441
+ continue;
442
+ }
443
+
444
+ if ((part as any).inlineData) {
445
+ parts.push({ inlineData: (part as any).inlineData });
446
+ }
447
+ }
448
+ } else {
449
+ if (typeof messageBlock.content === 'string' && messageBlock.content.trim()) {
450
+ parts.push({ text: messageBlock.content.trim() });
451
+ } else if (Array.isArray(messageBlock.content) && messageBlock.content.length > 0) {
452
+ parts.push(...messageBlock.content);
453
+ }
390
454
  }
391
455
 
392
- if (messageBlock.tool_calls) {
393
- const functionCalls = messageBlock.tool_calls.map((toolCall: any) => ({
394
- functionCall: {
395
- name: toolCall?.function?.name,
396
- args:
397
- typeof toolCall?.function?.arguments === 'string'
398
- ? JSON.parse(toolCall.function.arguments)
399
- : toolCall?.function?.arguments || {},
400
- },
401
- }));
456
+ if (Array.isArray(messageBlock.tool_calls) && messageBlock.tool_calls.length > 0) {
457
+ const functionCalls = messageBlock.tool_calls
458
+ .map((toolCall: any) => {
459
+ if (!toolCall?.function?.name) return undefined;
460
+ return {
461
+ functionCall: {
462
+ name: toolCall.function.name,
463
+ args: parseFunctionArgs(toolCall.function.arguments),
464
+ },
465
+ };
466
+ })
467
+ .filter(Boolean);
468
+
402
469
  parts.push(...functionCalls);
403
470
  }
404
471
 
405
- messageBlocks.push({
406
- role: messageBlock.role,
407
- parts,
408
- });
472
+ const hasFunctionCall = parts.some((part) => part.functionCall);
473
+ if (!hasFunctionCall && toolsData.length > 0) {
474
+ toolsData.forEach((toolCall) => {
475
+ parts.push({
476
+ functionCall: {
477
+ name: toolCall.name,
478
+ args: parseFunctionArgs(toolCall.arguments),
479
+ },
480
+ });
481
+ });
482
+ }
483
+
484
+ if (parts.length > 0) {
485
+ let role = messageBlock.role;
486
+ if (role === TLLMMessageRole.Assistant) {
487
+ role = TLLMMessageRole.Model;
488
+ } else if (role === TLLMMessageRole.Tool) {
489
+ role = TLLMMessageRole.Function;
490
+ }
491
+
492
+ messageBlocks.push({
493
+ role,
494
+ parts,
495
+ });
496
+ }
409
497
  }
410
498
 
411
499
  // Transform tool results
412
- const toolResults = toolsData.map((toolData) => ({
413
- role: TLLMMessageRole.User,
414
- parts: [
415
- {
416
- functionResponse: {
417
- name: toolData.name,
418
- response: {
419
- name: toolData.name,
420
- content: toolData.result,
421
- },
422
- },
500
+ const functionResponseParts = toolsData
501
+ .filter((toolData) => toolData.result !== undefined)
502
+ .map((toolData) => ({
503
+ functionResponse: {
504
+ name: toolData.name,
505
+ response: parseFunctionResponse(toolData.result),
423
506
  },
424
- ],
425
- }));
507
+ }));
508
+
509
+ if (functionResponseParts.length > 0) {
510
+ messageBlocks.push({
511
+ role: TLLMMessageRole.Function,
512
+ parts: functionResponseParts,
513
+ });
514
+ }
426
515
 
427
- messageBlocks.push(...toolResults);
428
516
  return messageBlocks;
429
517
  }
430
518
  }
@@ -28,6 +28,7 @@ import { Logger } from '@sre/helpers/Log.helper';
28
28
  import { LLMConnector } from '../../LLMConnector';
29
29
  import { OpenAIApiInterface, OpenAIApiInterfaceFactory } from './apiInterfaces';
30
30
  import { HandlerDependencies } from './types';
31
+ import { hookAsync } from '@sre/Core/HookService';
31
32
 
32
33
  const logger = Logger('OpenAIConnector');
33
34
 
@@ -69,31 +70,38 @@ export class OpenAIConnector extends LLMConnector {
69
70
  return responseInterface;
70
71
  }
71
72
 
72
- protected async getClient(params: ILLMRequestContext): Promise<OpenAI> {
73
- const apiKey = (params.credentials as BasicCredentials)?.apiKey;
74
- const baseURL = params?.modelInfo?.baseURL;
73
+ protected async getClient(context: ILLMRequestContext): Promise<OpenAI> {
74
+ const apiKey = (context.credentials as BasicCredentials)?.apiKey || '';
75
+ const baseURL = context?.modelInfo?.baseURL;
75
76
 
76
- const openai = new OpenAI({ baseURL, apiKey });
77
+ try {
78
+ const openai = new OpenAI({ baseURL, apiKey });
77
79
 
78
- return openai;
80
+ return openai;
81
+ } catch (error) {
82
+ console.error('Error: on OpenAI client initialization', error);
83
+ throw error;
84
+ }
79
85
  }
80
86
 
87
+ @hookAsync('LLMConnector.request')
81
88
  protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
82
89
  try {
83
90
  logger.debug(`request ${this.name}`, acRequest.candidate);
84
91
  const _body = body as OpenAI.ChatCompletionCreateParams;
85
92
 
86
- // #region Validate token limit
87
- const messages = _body?.messages || [];
88
- const lastMessage = messages[messages.length - 1];
89
- const promptTokens = await this.computePromptTokens(messages, context);
90
-
91
- await this.validateTokenLimit({
92
- acRequest,
93
- promptTokens,
94
- context,
95
- maxTokens: _body.max_completion_tokens,
96
- });
93
+ // #region Validate the token limit only if it's a legacy model.
94
+ if (context?.modelEntryName?.startsWith('legacy/')) {
95
+ const messages = _body?.messages || [];
96
+ const promptTokens = await this.computePromptTokens(messages, context);
97
+
98
+ await this.validateTokenLimit({
99
+ acRequest,
100
+ promptTokens,
101
+ context,
102
+ maxTokens: _body.max_completion_tokens,
103
+ });
104
+ }
97
105
  // #endregion Validate token limit
98
106
 
99
107
  const responseInterface = this.getInterfaceType(context);
@@ -143,20 +151,23 @@ export class OpenAIConnector extends LLMConnector {
143
151
  }
144
152
  }
145
153
 
154
+ @hookAsync('LLMConnector.streamRequest')
146
155
  protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
147
156
  try {
148
157
  logger.debug(`streamRequest ${this.name}`, acRequest.candidate);
149
- // #region Validate token limit
150
- const messages = body?.messages || body?.input || [];
151
- const lastMessage = messages[messages.length - 1];
152
- const promptTokens = await this.computePromptTokens(messages, context);
153
-
154
- await this.validateTokenLimit({
155
- acRequest,
156
- promptTokens,
157
- context,
158
- maxTokens: body.max_completion_tokens,
159
- });
158
+
159
+ // #region Validate the token limit only if it's a legacy model.
160
+ if (context?.modelEntryName?.startsWith('legacy/')) {
161
+ const messages = body?.messages || body?.input || [];
162
+ const promptTokens = await this.computePromptTokens(messages, context);
163
+
164
+ await this.validateTokenLimit({
165
+ acRequest,
166
+ promptTokens,
167
+ context,
168
+ maxTokens: body.max_completion_tokens,
169
+ });
170
+ }
160
171
  // #endregion Validate token limit
161
172
 
162
173
  const responseInterface = this.getInterfaceType(context);
@@ -389,9 +389,10 @@ export class ChatCompletionsApiInterface extends OpenAIApiInterface {
389
389
  * Upload files to storage
390
390
  */
391
391
  private async uploadFiles(files: BinaryInput[], agentId: string): Promise<BinaryInput[]> {
392
+ const candidate = AccessCandidate.agent(agentId);
392
393
  const promises = files.map((file) => {
393
- const binaryInput = BinaryInput.from(file);
394
- return binaryInput.upload(AccessCandidate.agent(agentId)).then(() => binaryInput);
394
+ const binaryInput = BinaryInput.from(file, null, null, candidate);
395
+ return binaryInput.upload(candidate).then(() => binaryInput);
395
396
  });
396
397
 
397
398
  return Promise.all(promises);
@@ -1,6 +1,9 @@
1
1
  import EventEmitter from 'events';
2
2
  import OpenAI from 'openai';
3
3
  import type { Stream } from 'openai/streaming';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import fs from 'fs';
4
7
 
5
8
  import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
6
9
  import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
@@ -10,6 +13,7 @@ import { HandlerDependencies, TToolType } from '../types';
10
13
  import { SUPPORTED_MIME_TYPES_MAP } from '@sre/constants';
11
14
  import { SEARCH_TOOL_COSTS } from './constants';
12
15
  import { isValidOpenAIReasoningEffort } from './utils';
16
+ import { uid } from '@sre/utils';
13
17
 
14
18
  // File size limits in bytes
15
19
  const MAX_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB
@@ -30,6 +34,7 @@ const EVENT_TYPES = {
30
34
  FUNCTION_CALL_ARGUMENTS_DELTA: 'response.function_call_arguments.delta',
31
35
  FUNCTION_CALL_ARGUMENTS_DONE: 'response.function_call_arguments.done',
32
36
  OUTPUT_ITEM_DONE: 'response.output_item.done',
37
+ INCOMPLETE: 'response.incomplete',
33
38
  } as const;
34
39
 
35
40
  // Type definitions for web search events (augmenting SDK types locally)
@@ -176,6 +181,14 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
176
181
  break;
177
182
  }
178
183
 
184
+ case EVENT_TYPES.INCOMPLETE:
185
+ finishReason = 'incomplete';
186
+ const responseData = (part as any)?.response;
187
+ if (responseData?.usage) {
188
+ usageData.push(responseData.usage);
189
+ }
190
+ break;
191
+
179
192
  default: {
180
193
  const eventType = String(part.type);
181
194
  // Handle legacy started event if ever emitted
@@ -257,12 +270,12 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
257
270
 
258
271
  // Emit interrupted event if finishReason is not 'stop'
259
272
  if (finishReason !== 'stop') {
260
- emitter.emit('interrupted', finishReason);
273
+ emitter.emit(TLLMEvent.Interrupted, finishReason);
261
274
  }
262
275
 
263
276
  // Emit end event with setImmediate to ensure proper event ordering
264
277
  setImmediate(() => {
265
- emitter.emit('end', toolsData, reportedUsage, finishReason);
278
+ emitter.emit(TLLMEvent.End, toolsData, reportedUsage, finishReason);
266
279
  });
267
280
  }
268
281
 
@@ -790,8 +803,62 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
790
803
  return Promise.all(promises);
791
804
  }
792
805
 
806
+ /**
807
+ * Upload file to OpenAI Files API
808
+ * Similar to GoogleAI's uploadFile implementation
809
+ */
810
+ private async uploadFile({
811
+ file,
812
+ agentId,
813
+ purpose = 'user_data',
814
+ }: {
815
+ file: BinaryInput;
816
+ agentId: string;
817
+ purpose?: 'user_data' | 'assistants' | 'batch' | 'fine-tune' | 'vision';
818
+ }): Promise<{ fileId: string; filename: string }> {
819
+ try {
820
+ if (!file?.mimetype) {
821
+ throw new Error('Missing required parameters to upload file to OpenAI!');
822
+ }
823
+
824
+ const tempDir = os.tmpdir();
825
+ const fileName = await file.getName();
826
+ const tempFilePath = path.join(tempDir, `${uid()}_${fileName}`);
827
+
828
+ // Write file to temporary location
829
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
830
+ await fs.promises.writeFile(tempFilePath, new Uint8Array(bufferData));
831
+
832
+ const openai = await this.deps.getClient(this.context);
833
+
834
+ // Upload file to OpenAI Files API
835
+ const uploadResponse = await openai.files.create({
836
+ file: fs.createReadStream(tempFilePath),
837
+ purpose: purpose,
838
+ });
839
+
840
+ const fileId = uploadResponse.id;
841
+ if (!fileId) {
842
+ throw new Error('File upload did not return a file ID.');
843
+ }
844
+
845
+ // Clean up temporary file
846
+ await fs.promises.unlink(tempFilePath).catch(() => {
847
+ // Ignore cleanup errors
848
+ });
849
+
850
+ return {
851
+ fileId,
852
+ filename: fileName,
853
+ };
854
+ } catch (error: any) {
855
+ throw new Error(`Error uploading file to OpenAI: ${error.message}`);
856
+ }
857
+ }
858
+
793
859
  /**
794
860
  * Process image files with Responses API specific formatting
861
+ * Uses OpenAI Files API for uploading images
795
862
  */
796
863
  private async processImageData(files: BinaryInput[], agentId: string): Promise<any[]> {
797
864
  if (files.length === 0) return [];
@@ -800,14 +867,30 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
800
867
  for (const file of files) {
801
868
  await this.validateFileSize(file, MAX_IMAGE_SIZE, 'Image');
802
869
 
803
- const bufferData = await file.readData(AccessCandidate.agent(agentId));
804
- const base64Data = bufferData.toString('base64');
805
- const url = `data:${file.mimetype};base64,${base64Data}`;
870
+ try {
871
+ // Upload file to OpenAI Files API with 'vision' purpose
872
+ const { fileId } = await this.uploadFile({
873
+ file,
874
+ agentId,
875
+ purpose: 'vision',
876
+ });
806
877
 
807
- imageData.push({
808
- type: 'input_image',
809
- image_url: url,
810
- });
878
+ imageData.push({
879
+ type: 'input_image',
880
+ file_id: fileId,
881
+ });
882
+ } catch (error) {
883
+ // If Files API upload fails, fall back to base64 inline data
884
+ console.warn('Failed to upload image via Files API, falling back to base64:', error);
885
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
886
+ const base64Data = bufferData.toString('base64');
887
+ const url = `data:${file.mimetype};base64,${base64Data}`;
888
+
889
+ imageData.push({
890
+ type: 'input_image',
891
+ image_url: url,
892
+ });
893
+ }
811
894
  }
812
895
 
813
896
  return imageData;
@@ -815,6 +898,7 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
815
898
 
816
899
  /**
817
900
  * Process document files with Responses API specific formatting
901
+ * Uses OpenAI Files API for uploading documents
818
902
  */
819
903
  private async processDocumentData(files: BinaryInput[], agentId: string): Promise<any[]> {
820
904
  if (files.length === 0) return [];
@@ -823,18 +907,31 @@ export class ResponsesApiInterface extends OpenAIApiInterface {
823
907
  for (const file of files) {
824
908
  await this.validateFileSize(file, MAX_DOCUMENT_SIZE, 'Document');
825
909
 
826
- const bufferData = await file.readData(AccessCandidate.agent(agentId));
827
- const base64Data = bufferData.toString('base64');
828
- const fileData = `data:${file.mimetype};base64,${base64Data}`;
829
- const filename = await file.getName();
830
-
831
- documentData.push({
832
- type: 'input_file',
833
- file: {
834
- file_data: fileData,
910
+ try {
911
+ // Upload file to OpenAI Files API with 'user_data' purpose
912
+ const { fileId, filename } = await this.uploadFile({
913
+ file,
914
+ agentId,
915
+ purpose: 'user_data',
916
+ });
917
+
918
+ documentData.push({
919
+ type: 'input_file',
920
+ file_id: fileId,
921
+ });
922
+ } catch (error) {
923
+ // If Files API upload fails, fall back to base64 inline data
924
+ console.warn('Failed to upload document via Files API, falling back to base64:', error);
925
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
926
+ const base64Data = bufferData.toString('base64');
927
+ const filename = await file.getName();
928
+
929
+ documentData.push({
930
+ type: 'input_file',
835
931
  filename,
836
- },
837
- });
932
+ file_data: `data:${file.mimetype};base64,${base64Data}`,
933
+ });
934
+ }
838
935
  }
839
936
 
840
937
  return documentData;
@@ -20,6 +20,7 @@ import { LLMHelper } from '@sre/LLMManager/LLM.helper';
20
20
  import { LLMConnector } from '../LLMConnector';
21
21
  import { SystemEvents } from '@sre/Core/SystemEvents';
22
22
  import { Logger } from '@sre/helpers/Log.helper';
23
+ import { hookAsync } from '@sre/Core/HookService';
23
24
 
24
25
  const logger = Logger('xAIConnector');
25
26
 
@@ -97,6 +98,7 @@ export class xAIConnector extends LLMConnector {
97
98
  });
98
99
  }
99
100
 
101
+ @hookAsync('LLMConnector.request')
100
102
  protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
101
103
  try {
102
104
  logger.debug(`request ${this.name}`, acRequest.candidate);
@@ -153,6 +155,7 @@ export class xAIConnector extends LLMConnector {
153
155
  }
154
156
  }
155
157
 
158
+ @hookAsync('LLMConnector.streamRequest')
156
159
  protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
157
160
  const emitter = new EventEmitter();
158
161
 
@@ -193,11 +193,11 @@ export abstract class ModelsProviderConnector extends SecureConnector {
193
193
  modelInfo = models?.[model as string];
194
194
  }
195
195
 
196
+ // TODO: We will clean up `keyOptions` in the future but keep it for legacy users.
196
197
  const aliasKeyOptions = aliasModelInfo && hasAPIKey ? aliasModelInfo?.keyOptions : null;
197
-
198
198
  const modelKeyOptions = modelInfo?.keyOptions || aliasKeyOptions;
199
199
 
200
- return { ...aliasModelInfo, ...modelInfo, ...aliasKeyOptions, ...modelKeyOptions, modelId };
200
+ return { ...modelInfo, ...aliasModelInfo, ...aliasKeyOptions, ...modelKeyOptions, modelId };
201
201
  }
202
202
 
203
203
  protected async getModelId(acRequest: AccessRequest, models: TLLMModelsList, model: string | TLLMModel | TCustomLLMModel): Promise<string> {
@@ -394,12 +394,7 @@ export abstract class ModelsProviderConnector extends SecureConnector {
394
394
  baseURL: entry.baseURL,
395
395
  fallbackLLM: entry.fallbackLLM,
396
396
  isUserCustomLLM: true,
397
-
398
- // TODO: Credentials will usually look like { apiKey: 'api-key-goes-here' }.
399
- // However, for fallback models we also need to handle ['vault', 'internal']
400
- // using the same credentials format, since fallback models can be either
401
- // personal or built-in.
402
- credentials: entry?.credentials || ['vault', 'internal'],
397
+ credentials: entry?.credentials || {},
403
398
  };
404
399
  }
405
400
 
@@ -170,11 +170,13 @@ export class JSONModelsProvider extends ModelsProviderConnector {
170
170
  }
171
171
  } else if (typeof modelData === 'object' && !Array.isArray(modelData)) {
172
172
  // Object of models case
173
+ let models = '';
173
174
  for (const [modelId, model] of Object.entries(modelData)) {
174
175
  try {
175
176
  if (this.isValidSingleModel(model)) {
176
177
  validModels[modelId] = model as TLLMModel;
177
- console.debug(`Loaded model: ${modelId}`);
178
+ //console.debug(`Loaded model: ${modelId}`);
179
+ models += `${modelId} `;
178
180
  } else {
179
181
  console.warn(`Invalid model format for model "${modelId}"`);
180
182
  }
@@ -183,6 +185,7 @@ export class JSONModelsProvider extends ModelsProviderConnector {
183
185
  // Continue processing other models instead of failing the whole file
184
186
  }
185
187
  }
188
+ console.debug(`Loaded models: ${models}`);
186
189
  } else {
187
190
  console.warn(`Invalid format (not a model or object of models)`);
188
191
  }
@@ -111,6 +111,18 @@ export class RedisCache extends CacheConnector {
111
111
 
112
112
  @SecureConnector.AccessControl
113
113
  public async setMetadata(acRequest: AccessRequest, key: string, metadata: CacheMetadata): Promise<void> {
114
+ if (metadata.acl) {
115
+ //preserve the ownership of the metadata
116
+ const newACL = ACL.from(metadata.acl).addAccess(acRequest.candidate.role, acRequest.candidate.id, TAccessLevel.Owner).ACL;
117
+ metadata.acl = newACL;
118
+ }
119
+
120
+ //no ACL present ==> preserve the existing ACL
121
+ if (!metadata.acl) {
122
+ const curACL = await this.getACL(acRequest, key);
123
+ metadata.acl = curACL;
124
+ }
125
+
114
126
  await this.setMetadataWithTTL(acRequest, key, metadata);
115
127
  }
116
128
  private async setMetadataWithTTL(acRequest: AccessRequest, key: string, metadata: CacheMetadata, ttl?: number): Promise<void> {
@@ -25,7 +25,7 @@ export class LLMContext {
25
25
  public contextLength: number;
26
26
 
27
27
  private _messages: any[] = [];
28
- public get messages() {
28
+ public get messages() {
29
29
  return this._messages;
30
30
  }
31
31
 
@@ -36,16 +36,11 @@ export class LLMContext {
36
36
  *
37
37
  * @param source a messages[] object, or smyth file system uri (smythfs://...)
38
38
  */
39
- constructor(
40
- private llmInference,
41
- _systemPrompt: string = '',
42
- llmContextStore?: ILLMContextStore,
43
- ) {
39
+ constructor(private llmInference, _systemPrompt: string = '', llmContextStore?: ILLMContextStore) {
44
40
  this._llmCache = new LLMCache(AccessCandidate.team(this.llmInference.teamId));
45
41
  //this._systemPrompt = _systemPrompt;
46
42
  this.systemPrompt = _systemPrompt;
47
43
 
48
-
49
44
  if (llmContextStore) {
50
45
  this._llmContextStore = llmContextStore;
51
46
  this._llmContextStore.load().then((messages) => {
@@ -57,7 +52,7 @@ export class LLMContext {
57
52
 
58
53
  private push(...message: any[]) {
59
54
  this._messages.push(...message);
60
-
55
+
61
56
  if (this._llmContextStore) {
62
57
  this._llmContextStore.save(this._messages);
63
58
  }