@smythos/sre 1.5.0 → 1.5.2

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 (189) hide show
  1. package/CHANGELOG +62 -0
  2. package/LICENSE +18 -0
  3. package/package.json +127 -115
  4. package/src/Components/APICall/APICall.class.ts +155 -0
  5. package/src/Components/APICall/AccessTokenManager.ts +130 -0
  6. package/src/Components/APICall/ArrayBufferResponse.helper.ts +58 -0
  7. package/src/Components/APICall/OAuth.helper.ts +294 -0
  8. package/src/Components/APICall/mimeTypeCategories.ts +46 -0
  9. package/src/Components/APICall/parseData.ts +167 -0
  10. package/src/Components/APICall/parseHeaders.ts +41 -0
  11. package/src/Components/APICall/parseProxy.ts +68 -0
  12. package/src/Components/APICall/parseUrl.ts +91 -0
  13. package/src/Components/APIEndpoint.class.ts +234 -0
  14. package/src/Components/APIOutput.class.ts +58 -0
  15. package/src/Components/AgentPlugin.class.ts +102 -0
  16. package/src/Components/Async.class.ts +155 -0
  17. package/src/Components/Await.class.ts +90 -0
  18. package/src/Components/Classifier.class.ts +158 -0
  19. package/src/Components/Component.class.ts +94 -0
  20. package/src/Components/ComponentHost.class.ts +38 -0
  21. package/src/Components/DataSourceCleaner.class.ts +92 -0
  22. package/src/Components/DataSourceIndexer.class.ts +181 -0
  23. package/src/Components/DataSourceLookup.class.ts +141 -0
  24. package/src/Components/FEncDec.class.ts +29 -0
  25. package/src/Components/FHash.class.ts +33 -0
  26. package/src/Components/FSign.class.ts +80 -0
  27. package/src/Components/FSleep.class.ts +25 -0
  28. package/src/Components/FTimestamp.class.ts +25 -0
  29. package/src/Components/FileStore.class.ts +75 -0
  30. package/src/Components/ForEach.class.ts +97 -0
  31. package/src/Components/GPTPlugin.class.ts +70 -0
  32. package/src/Components/GenAILLM.class.ts +395 -0
  33. package/src/Components/HuggingFace.class.ts +314 -0
  34. package/src/Components/Image/imageSettings.config.ts +70 -0
  35. package/src/Components/ImageGenerator.class.ts +407 -0
  36. package/src/Components/JSONFilter.class.ts +54 -0
  37. package/src/Components/LLMAssistant.class.ts +213 -0
  38. package/src/Components/LogicAND.class.ts +28 -0
  39. package/src/Components/LogicAtLeast.class.ts +85 -0
  40. package/src/Components/LogicAtMost.class.ts +86 -0
  41. package/src/Components/LogicOR.class.ts +29 -0
  42. package/src/Components/LogicXOR.class.ts +34 -0
  43. package/src/Components/MCPClient.class.ts +112 -0
  44. package/src/Components/PromptGenerator.class.ts +122 -0
  45. package/src/Components/ScrapflyWebScrape.class.ts +159 -0
  46. package/src/Components/TavilyWebSearch.class.ts +98 -0
  47. package/src/Components/index.ts +77 -0
  48. package/src/Core/AgentProcess.helper.ts +240 -0
  49. package/src/Core/Connector.class.ts +123 -0
  50. package/src/Core/ConnectorsService.ts +192 -0
  51. package/src/Core/DummyConnector.ts +49 -0
  52. package/src/Core/HookService.ts +105 -0
  53. package/src/Core/SmythRuntime.class.ts +292 -0
  54. package/src/Core/SystemEvents.ts +15 -0
  55. package/src/Core/boot.ts +55 -0
  56. package/src/config.ts +15 -0
  57. package/src/constants.ts +125 -0
  58. package/src/data/hugging-face.params.json +580 -0
  59. package/src/helpers/BinaryInput.helper.ts +324 -0
  60. package/src/helpers/Conversation.helper.ts +1094 -0
  61. package/src/helpers/JsonContent.helper.ts +97 -0
  62. package/src/helpers/LocalCache.helper.ts +97 -0
  63. package/src/helpers/Log.helper.ts +234 -0
  64. package/src/helpers/OpenApiParser.helper.ts +150 -0
  65. package/src/helpers/S3Cache.helper.ts +129 -0
  66. package/src/helpers/SmythURI.helper.ts +5 -0
  67. package/src/helpers/TemplateString.helper.ts +243 -0
  68. package/src/helpers/TypeChecker.helper.ts +329 -0
  69. package/src/index.ts +179 -0
  70. package/src/index.ts.bak +179 -0
  71. package/src/subsystems/AgentManager/Agent.class.ts +1108 -0
  72. package/src/subsystems/AgentManager/Agent.helper.ts +3 -0
  73. package/src/subsystems/AgentManager/AgentData.service/AgentDataConnector.ts +230 -0
  74. package/src/subsystems/AgentManager/AgentData.service/connectors/CLIAgentDataConnector.class.ts +66 -0
  75. package/src/subsystems/AgentManager/AgentData.service/connectors/LocalAgentDataConnector.class.ts +142 -0
  76. package/src/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.ts +39 -0
  77. package/src/subsystems/AgentManager/AgentData.service/index.ts +18 -0
  78. package/src/subsystems/AgentManager/AgentLogger.class.ts +297 -0
  79. package/src/subsystems/AgentManager/AgentRequest.class.ts +51 -0
  80. package/src/subsystems/AgentManager/AgentRuntime.class.ts +559 -0
  81. package/src/subsystems/AgentManager/AgentSSE.class.ts +101 -0
  82. package/src/subsystems/AgentManager/AgentSettings.class.ts +52 -0
  83. package/src/subsystems/AgentManager/Component.service/ComponentConnector.ts +32 -0
  84. package/src/subsystems/AgentManager/Component.service/connectors/LocalComponentConnector.class.ts +59 -0
  85. package/src/subsystems/AgentManager/Component.service/index.ts +11 -0
  86. package/src/subsystems/AgentManager/EmbodimentSettings.class.ts +47 -0
  87. package/src/subsystems/AgentManager/ForkedAgent.class.ts +153 -0
  88. package/src/subsystems/AgentManager/OSResourceMonitor.ts +77 -0
  89. package/src/subsystems/ComputeManager/Code.service/CodeConnector.ts +99 -0
  90. package/src/subsystems/ComputeManager/Code.service/connectors/AWSLambdaCode.class.ts +63 -0
  91. package/src/subsystems/ComputeManager/Code.service/index.ts +11 -0
  92. package/src/subsystems/IO/CLI.service/CLIConnector.ts +47 -0
  93. package/src/subsystems/IO/CLI.service/index.ts +9 -0
  94. package/src/subsystems/IO/Log.service/LogConnector.ts +32 -0
  95. package/src/subsystems/IO/Log.service/connectors/ConsoleLog.class.ts +28 -0
  96. package/src/subsystems/IO/Log.service/index.ts +13 -0
  97. package/src/subsystems/IO/NKV.service/NKVConnector.ts +41 -0
  98. package/src/subsystems/IO/NKV.service/connectors/NKVRAM.class.ts +204 -0
  99. package/src/subsystems/IO/NKV.service/connectors/NKVRedis.class.ts +182 -0
  100. package/src/subsystems/IO/NKV.service/index.ts +12 -0
  101. package/src/subsystems/IO/Router.service/RouterConnector.ts +21 -0
  102. package/src/subsystems/IO/Router.service/connectors/ExpressRouter.class.ts +48 -0
  103. package/src/subsystems/IO/Router.service/connectors/NullRouter.class.ts +40 -0
  104. package/src/subsystems/IO/Router.service/index.ts +11 -0
  105. package/src/subsystems/IO/Storage.service/SmythFS.class.ts +472 -0
  106. package/src/subsystems/IO/Storage.service/StorageConnector.ts +66 -0
  107. package/src/subsystems/IO/Storage.service/connectors/LocalStorage.class.ts +305 -0
  108. package/src/subsystems/IO/Storage.service/connectors/S3Storage.class.ts +418 -0
  109. package/src/subsystems/IO/Storage.service/index.ts +13 -0
  110. package/src/subsystems/IO/VectorDB.service/VectorDBConnector.ts +108 -0
  111. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +450 -0
  112. package/src/subsystems/IO/VectorDB.service/connectors/PineconeVectorDB.class.ts +373 -0
  113. package/src/subsystems/IO/VectorDB.service/connectors/RAMVecrtorDB.class.ts +420 -0
  114. package/src/subsystems/IO/VectorDB.service/embed/BaseEmbedding.ts +106 -0
  115. package/src/subsystems/IO/VectorDB.service/embed/OpenAIEmbedding.ts +109 -0
  116. package/src/subsystems/IO/VectorDB.service/embed/index.ts +21 -0
  117. package/src/subsystems/IO/VectorDB.service/index.ts +14 -0
  118. package/src/subsystems/LLMManager/LLM.helper.ts +221 -0
  119. package/src/subsystems/LLMManager/LLM.inference.ts +335 -0
  120. package/src/subsystems/LLMManager/LLM.service/LLMConnector.ts +374 -0
  121. package/src/subsystems/LLMManager/LLM.service/LLMCredentials.helper.ts +145 -0
  122. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +632 -0
  123. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +405 -0
  124. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +81 -0
  125. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +689 -0
  126. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +257 -0
  127. package/src/subsystems/LLMManager/LLM.service/connectors/OpenAI.class.ts +848 -0
  128. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +255 -0
  129. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +193 -0
  130. package/src/subsystems/LLMManager/LLM.service/index.ts +43 -0
  131. package/src/subsystems/LLMManager/ModelsProvider.service/ModelsProviderConnector.ts +281 -0
  132. package/src/subsystems/LLMManager/ModelsProvider.service/connectors/SmythModelsProvider.class.ts +229 -0
  133. package/src/subsystems/LLMManager/ModelsProvider.service/index.ts +11 -0
  134. package/src/subsystems/LLMManager/custom-models.ts +854 -0
  135. package/src/subsystems/LLMManager/models.ts +2539 -0
  136. package/src/subsystems/LLMManager/paramMappings.ts +69 -0
  137. package/src/subsystems/MemoryManager/Cache.service/CacheConnector.ts +86 -0
  138. package/src/subsystems/MemoryManager/Cache.service/connectors/LocalStorageCache.class.ts +297 -0
  139. package/src/subsystems/MemoryManager/Cache.service/connectors/RAMCache.class.ts +201 -0
  140. package/src/subsystems/MemoryManager/Cache.service/connectors/RedisCache.class.ts +252 -0
  141. package/src/subsystems/MemoryManager/Cache.service/connectors/S3Cache.class.ts +373 -0
  142. package/src/subsystems/MemoryManager/Cache.service/index.ts +15 -0
  143. package/src/subsystems/MemoryManager/LLMCache.ts +72 -0
  144. package/src/subsystems/MemoryManager/LLMContext.ts +125 -0
  145. package/src/subsystems/MemoryManager/RuntimeContext.ts +249 -0
  146. package/src/subsystems/Security/AccessControl/ACL.class.ts +208 -0
  147. package/src/subsystems/Security/AccessControl/AccessCandidate.class.ts +76 -0
  148. package/src/subsystems/Security/AccessControl/AccessRequest.class.ts +52 -0
  149. package/src/subsystems/Security/Account.service/AccountConnector.ts +41 -0
  150. package/src/subsystems/Security/Account.service/connectors/AWSAccount.class.ts +76 -0
  151. package/src/subsystems/Security/Account.service/connectors/DummyAccount.class.ts +130 -0
  152. package/src/subsystems/Security/Account.service/connectors/JSONFileAccount.class.ts +159 -0
  153. package/src/subsystems/Security/Account.service/index.ts +14 -0
  154. package/src/subsystems/Security/Credentials.helper.ts +62 -0
  155. package/src/subsystems/Security/ManagedVault.service/ManagedVaultConnector.ts +34 -0
  156. package/src/subsystems/Security/ManagedVault.service/connectors/NullManagedVault.class.ts +57 -0
  157. package/src/subsystems/Security/ManagedVault.service/connectors/SecretManagerManagedVault.ts +154 -0
  158. package/src/subsystems/Security/ManagedVault.service/index.ts +12 -0
  159. package/src/subsystems/Security/SecureConnector.class.ts +110 -0
  160. package/src/subsystems/Security/Vault.service/Vault.helper.ts +30 -0
  161. package/src/subsystems/Security/Vault.service/VaultConnector.ts +26 -0
  162. package/src/subsystems/Security/Vault.service/connectors/HashicorpVault.class.ts +46 -0
  163. package/src/subsystems/Security/Vault.service/connectors/JSONFileVault.class.ts +166 -0
  164. package/src/subsystems/Security/Vault.service/connectors/NullVault.class.ts +54 -0
  165. package/src/subsystems/Security/Vault.service/connectors/SecretsManager.class.ts +140 -0
  166. package/src/subsystems/Security/Vault.service/index.ts +12 -0
  167. package/src/types/ACL.types.ts +104 -0
  168. package/src/types/AWS.types.ts +9 -0
  169. package/src/types/Agent.types.ts +61 -0
  170. package/src/types/AgentLogger.types.ts +17 -0
  171. package/src/types/Cache.types.ts +1 -0
  172. package/src/types/Common.types.ts +3 -0
  173. package/src/types/LLM.types.ts +419 -0
  174. package/src/types/Redis.types.ts +8 -0
  175. package/src/types/SRE.types.ts +64 -0
  176. package/src/types/Security.types.ts +18 -0
  177. package/src/types/Storage.types.ts +5 -0
  178. package/src/types/VectorDB.types.ts +78 -0
  179. package/src/utils/base64.utils.ts +275 -0
  180. package/src/utils/cli.utils.ts +68 -0
  181. package/src/utils/data.utils.ts +263 -0
  182. package/src/utils/date-time.utils.ts +22 -0
  183. package/src/utils/general.utils.ts +238 -0
  184. package/src/utils/index.ts +12 -0
  185. package/src/utils/numbers.utils.ts +13 -0
  186. package/src/utils/oauth.utils.ts +35 -0
  187. package/src/utils/string.utils.ts +414 -0
  188. package/src/utils/url.utils.ts +19 -0
  189. package/src/utils/validation.utils.ts +74 -0
@@ -0,0 +1,848 @@
1
+ import EventEmitter from 'events';
2
+ import OpenAI, { toFile } from 'openai';
3
+ import type { Stream } from 'openai/streaming';
4
+ import { encodeChat } from 'gpt-tokenizer';
5
+
6
+ import { BUILT_IN_MODEL_PREFIX } from '@sre/constants';
7
+ import { BinaryInput } from '@sre/helpers/BinaryInput.helper';
8
+ import { AccessCandidate } from '@sre/Security/AccessControl/AccessCandidate.class';
9
+ import { AccessRequest } from '@sre/Security/AccessControl/AccessRequest.class';
10
+ import { LLMHelper } from '@sre/LLMManager/LLM.helper';
11
+ import { JSON_RESPONSE_INSTRUCTION, SUPPORTED_MIME_TYPES_MAP } from '@sre/constants';
12
+
13
+ import {
14
+ TLLMParams,
15
+ ToolData,
16
+ TLLMMessageBlock,
17
+ TLLMToolResultMessageBlock,
18
+ TLLMMessageRole,
19
+ APIKeySource,
20
+ TLLMEvent,
21
+ ILLMRequestFuncParams,
22
+ TOpenAIRequestBody,
23
+ TOpenAIResponseToolChoice,
24
+ TLLMChatResponse,
25
+ ILLMRequestContext,
26
+ BasicCredentials,
27
+ TLLMConnectorParams,
28
+ TLLMModel,
29
+ TCustomLLMModel,
30
+ ILLMConnectorCredentials,
31
+ } from '@sre/types/LLM.types';
32
+
33
+ import { LLMConnector } from '../LLMConnector';
34
+ import { SystemEvents } from '@sre/Core/SystemEvents';
35
+ import { ConnectorService } from '@sre/Core/ConnectorsService';
36
+
37
+ const MODELS_WITH_JSON_RESPONSE = ['gpt-4.5-preview', 'gpt-4o-2024-08-06', 'gpt-4o-mini-2024-07-18', 'gpt-4-turbo', 'gpt-3.5-turbo'];
38
+
39
+ type TSearchTool = 'web_search_preview';
40
+ type TSearchContextSize = 'low' | 'medium' | 'high';
41
+ type TSearchLocation = {
42
+ type: 'approximate';
43
+ city?: string;
44
+ country?: string;
45
+ region?: string;
46
+ timezone?: string;
47
+ };
48
+
49
+ // per 1k requests
50
+ const costForNormalModels = {
51
+ low: 30 / 1000,
52
+ medium: 35 / 1000,
53
+ high: 50 / 1000,
54
+ };
55
+ const costForMiniModels = {
56
+ low: 25 / 1000,
57
+ medium: 27.5 / 1000,
58
+ high: 30 / 1000,
59
+ };
60
+
61
+ const SEARCH_TOOL = {
62
+ type: 'web_search_preview' as TSearchTool,
63
+ cost: {
64
+ 'gpt-4.1': costForNormalModels,
65
+ 'gpt-4o': costForNormalModels,
66
+ 'gpt-4o-search': costForNormalModels,
67
+
68
+ 'gpt-4.1-mini': costForMiniModels,
69
+ 'gpt-4o-mini': costForMiniModels,
70
+ 'gpt-4o-mini-search': costForMiniModels,
71
+ },
72
+ };
73
+
74
+ export class OpenAIConnector extends LLMConnector {
75
+ public name = 'LLM:OpenAI';
76
+
77
+ //private openai: OpenAI;
78
+ private validImageMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.image;
79
+ private validDocumentMimeTypes = SUPPORTED_MIME_TYPES_MAP.OpenAI.document;
80
+
81
+ private async getClient(params: ILLMRequestContext): Promise<OpenAI> {
82
+ const apiKey = (params.credentials as BasicCredentials)?.apiKey;
83
+ const baseURL = params?.modelInfo?.baseURL;
84
+
85
+ if (!apiKey) throw new Error('Please provide an API key for OpenAI');
86
+
87
+ return new OpenAI({ baseURL, apiKey });
88
+ }
89
+
90
+ protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
91
+ const _body = body as OpenAI.ChatCompletionCreateParams;
92
+
93
+ try {
94
+ const openai = await this.getClient(context);
95
+ // #region Validate token limit
96
+ const messages = _body?.messages || [];
97
+ const lastMessage = messages[messages.length - 1];
98
+
99
+ const promptTokens = context?.hasFiles
100
+ ? await LLMHelper.countVisionPromptTokens(lastMessage?.content)
101
+ : encodeChat(messages as any, 'gpt-4')?.length;
102
+
103
+ await this.validateTokenLimit({
104
+ acRequest,
105
+ promptTokens,
106
+ context,
107
+ maxTokens: _body.max_completion_tokens,
108
+ });
109
+ // #endregion Validate token limit
110
+
111
+ const result = (await openai.chat.completions.create(_body)) as OpenAI.ChatCompletion;
112
+ const message = result?.choices?.[0]?.message;
113
+ const finishReason = result?.choices?.[0]?.finish_reason;
114
+
115
+ let toolsData: ToolData[] = [];
116
+ let useTool = false;
117
+
118
+ if (finishReason === 'tool_calls') {
119
+ toolsData =
120
+ message?.tool_calls?.map((tool, index) => ({
121
+ index,
122
+ id: tool?.id,
123
+ type: tool?.type,
124
+ name: tool?.function?.name,
125
+ arguments: tool?.function?.arguments,
126
+ role: 'tool',
127
+ })) || [];
128
+
129
+ useTool = true;
130
+ }
131
+
132
+ const usage = result?.usage;
133
+ this.reportUsage(usage, {
134
+ modelEntryName: context.modelEntryName,
135
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
136
+ agentId: context.agentId,
137
+ teamId: context.teamId,
138
+ });
139
+
140
+ return {
141
+ content: message?.content ?? '',
142
+ finishReason,
143
+ useTool,
144
+ toolsData,
145
+ message,
146
+ usage,
147
+ };
148
+ } catch (error: any) {
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
154
+ const _body = body as OpenAI.ChatCompletionCreateParams;
155
+
156
+ const emitter = new EventEmitter();
157
+ const usage_data: any[] = [];
158
+ const reportedUsage: any[] = [];
159
+
160
+ try {
161
+ const openai = await this.getClient(context);
162
+ // #region Validate token limit
163
+ const messages = _body?.messages || [];
164
+ const lastMessage = messages[messages.length - 1];
165
+
166
+ const promptTokens = context?.hasFiles
167
+ ? await LLMHelper.countVisionPromptTokens(lastMessage?.content)
168
+ : encodeChat(messages as any, 'gpt-4')?.length;
169
+
170
+ await this.validateTokenLimit({
171
+ acRequest,
172
+ promptTokens,
173
+ context,
174
+ maxTokens: _body.max_completion_tokens,
175
+ });
176
+ // #endregion Validate token limit
177
+
178
+ let finishReason = 'stop';
179
+
180
+ const stream = await openai.chat.completions.create({ ..._body, stream: true, stream_options: { include_usage: true } });
181
+
182
+ // Process stream asynchronously while as we need to return emitter immediately
183
+ (async () => {
184
+ let delta: Record<string, any> = {};
185
+
186
+ let toolsData: any = [];
187
+
188
+ for await (const part of stream) {
189
+ delta = part.choices[0]?.delta;
190
+ const usage = part.usage;
191
+
192
+ if (usage) {
193
+ usage_data.push(usage);
194
+ }
195
+ emitter.emit('data', delta);
196
+
197
+ if (!delta?.tool_calls && delta?.content) {
198
+ emitter.emit('content', delta?.content, delta?.role);
199
+ }
200
+ //_stream = toolCallsStream;
201
+ if (delta?.tool_calls) {
202
+ const toolCall = delta?.tool_calls?.[0];
203
+ const index = toolCall?.index;
204
+
205
+ toolsData[index] = {
206
+ index,
207
+ role: 'tool',
208
+ id: (toolsData?.[index]?.id || '') + (toolCall?.id || ''),
209
+ type: (toolsData?.[index]?.type || '') + (toolCall?.type || ''),
210
+ name: (toolsData?.[index]?.name || '') + (toolCall?.function?.name || ''),
211
+ arguments: (toolsData?.[index]?.arguments || '') + (toolCall?.function?.arguments || ''),
212
+ };
213
+
214
+ continue;
215
+ }
216
+ if (part.choices[0]?.finish_reason) {
217
+ finishReason = part.choices[0]?.finish_reason;
218
+ }
219
+ }
220
+ if (toolsData?.length > 0) {
221
+ for (let tool of toolsData) {
222
+ if (tool.type.includes('functionfunction')) {
223
+ tool.type = 'function'; //path wrong tool call generated by LM Studio
224
+ //FIXME: use cleaner method to fix wrong tool call formats
225
+ }
226
+ }
227
+ emitter.emit(TLLMEvent.ToolInfo, toolsData);
228
+ }
229
+
230
+ usage_data.forEach((usage) => {
231
+ // probably we can acc them and send them as one event
232
+ const _reported = this.reportUsage(usage, {
233
+ modelEntryName: context.modelEntryName,
234
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
235
+ agentId: context.agentId,
236
+ teamId: context.teamId,
237
+ });
238
+
239
+ reportedUsage.push(_reported);
240
+ });
241
+ if (finishReason !== 'stop') {
242
+ emitter.emit('interrupted', finishReason);
243
+ }
244
+
245
+ setTimeout(() => {
246
+ emitter.emit('end', toolsData, reportedUsage, finishReason);
247
+ }, 100);
248
+ })();
249
+ return emitter;
250
+ } catch (error: any) {
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ protected async webSearchRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
256
+ const _body = body as OpenAI.Responses.ResponseCreateParams;
257
+
258
+ const emitter = new EventEmitter();
259
+ const usage_data = [];
260
+ const reportedUsage = [];
261
+
262
+ try {
263
+ const openai = await this.getClient(context);
264
+ let finishReason = 'stop';
265
+ const stream = (await openai.responses.create(_body)) as Stream<OpenAI.Responses.ResponseStreamEvent>;
266
+
267
+ // Process stream asynchronously while we need to return emitter immediately
268
+ (async () => {
269
+ let toolsData: any = [];
270
+ let currentToolCall = null;
271
+
272
+ for await (const part of stream) {
273
+ // Handle different event types from the stream
274
+ if ('type' in part) {
275
+ const event = part.type;
276
+
277
+ switch (event) {
278
+ case 'response.output_text.delta': {
279
+ if (part?.delta) {
280
+ // Emit content in delta format for compatibility
281
+ const deltaMsg = {
282
+ role: 'assistant',
283
+ content: part.delta,
284
+ };
285
+ emitter.emit('data', deltaMsg);
286
+ emitter.emit('content', part.delta, 'assistant');
287
+ }
288
+ break;
289
+ }
290
+ // TODO: Handle other events
291
+ default: {
292
+ break;
293
+ }
294
+ }
295
+ }
296
+
297
+ if ('response' in part) {
298
+ // Handle usage statistics
299
+ if (part.response?.usage) {
300
+ usage_data.push(part.response.usage);
301
+ }
302
+ }
303
+ }
304
+
305
+ // Report usage statistics
306
+ const modelName = context.modelEntryName?.replace(BUILT_IN_MODEL_PREFIX, '');
307
+
308
+ const searchTool = _body.tools?.[0] as OpenAI.Responses.WebSearchTool;
309
+ const cost = SEARCH_TOOL.cost?.[modelName]?.[searchTool?.search_context_size] || 0;
310
+
311
+ this.reportUsage(
312
+ {
313
+ cost,
314
+
315
+ // 👇 Just to avoid a TypeScript error.
316
+ completion_tokens: 0,
317
+ prompt_tokens: 0,
318
+ total_tokens: 0,
319
+ // 👆 Just to avoid a TypeScript error.
320
+ },
321
+ {
322
+ modelEntryName: context.modelEntryName,
323
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
324
+ agentId: context.agentId,
325
+ teamId: context.teamId,
326
+ }
327
+ );
328
+
329
+ // Emit interrupted event if finishReason is not 'stop'
330
+ if (finishReason !== 'stop') {
331
+ emitter.emit('interrupted', finishReason);
332
+ }
333
+
334
+ // Emit end event with same data structure as v1
335
+ setTimeout(() => {
336
+ emitter.emit('end', toolsData, reportedUsage, finishReason);
337
+ }, 100);
338
+ })();
339
+
340
+ return emitter;
341
+ } catch (error: any) {
342
+ throw error;
343
+ }
344
+ }
345
+
346
+ // #region Image Generation, will be moved to a different subsystem
347
+ protected async imageGenRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<OpenAI.ImagesResponse> {
348
+ try {
349
+ const openai = await this.getClient(context);
350
+ const response = await openai.images.generate(body as OpenAI.Images.ImageGenerateParams);
351
+
352
+ return response;
353
+ } catch (error: any) {
354
+ throw error;
355
+ }
356
+ }
357
+
358
+ protected async imageEditRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<OpenAI.ImagesResponse> {
359
+ const _body = body as OpenAI.Images.ImageEditParams;
360
+
361
+ try {
362
+ const openai = await this.getClient(context);
363
+ const response = await openai.images.edit(_body);
364
+
365
+ return response;
366
+ } catch (error: any) {
367
+ throw error;
368
+ }
369
+ }
370
+
371
+ private async processImageData(files: BinaryInput[], agentId: string): Promise<any[]> {
372
+ const validSources = this.getValidImageFiles(files);
373
+ if (validSources.length === 0) {
374
+ return [];
375
+ }
376
+ return await this.getImageData(validSources, agentId);
377
+ }
378
+
379
+ private getValidDocumentFiles(files: BinaryInput[]): BinaryInput[] {
380
+ const validSources = [];
381
+ for (let file of files) {
382
+ if (this.validDocumentMimeTypes.includes(file?.mimetype)) {
383
+ validSources.push(file);
384
+ }
385
+ }
386
+
387
+ return validSources;
388
+ }
389
+
390
+ private async processDocumentData(files: BinaryInput[], agentId: string): Promise<any[]> {
391
+ const validSources = this.getValidDocumentFiles(files);
392
+ if (validSources.length === 0) {
393
+ return [];
394
+ }
395
+ return await this.getDocumentData(validSources, agentId);
396
+ }
397
+
398
+ protected async reqBodyAdapter(params: TLLMParams): Promise<TOpenAIRequestBody> {
399
+ // if it's web search request and the model has search capability, then we need to prepare the request body for the web search request
400
+ if (params?.useWebSearch && params.capabilities?.search === true) {
401
+ return this.prepareBodyForWebSearchRequest(params);
402
+ }
403
+
404
+ if (params.capabilities?.imageGeneration === true) {
405
+ if (params?.files?.length > 0) {
406
+ return this.prepareBodyForImageEditRequest(params);
407
+ } else {
408
+ return this.prepareBodyForImageGenRequest(params);
409
+ }
410
+ }
411
+
412
+ if (params.capabilities?.imageEditing === true) {
413
+ }
414
+
415
+ // In default, we will prepare the request body
416
+ return this.prepareBody(params);
417
+ }
418
+
419
+ protected reportUsage(
420
+ usage: OpenAI.Completions.CompletionUsage & {
421
+ input_tokens?: number;
422
+ output_tokens?: number;
423
+ input_tokens_details?: { cached_tokens?: number };
424
+ prompt_tokens_details?: { cached_tokens?: number };
425
+ cost?: number; // for web search tool
426
+ },
427
+ metadata: { modelEntryName: string; keySource: APIKeySource; agentId: string; teamId: string }
428
+ ) {
429
+ // SmythOS (built-in) models have a prefix, so we need to remove it to get the model name
430
+ const modelName = metadata.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
431
+
432
+ const inputTokens = usage?.input_tokens || usage?.prompt_tokens - (usage?.prompt_tokens_details?.cached_tokens || 0); // Returned by the search tool
433
+
434
+ const outputTokens =
435
+ usage?.output_tokens || // Returned by the search tool
436
+ usage?.completion_tokens ||
437
+ 0;
438
+
439
+ const cachedInputTokens =
440
+ usage?.input_tokens_details?.cached_tokens || // Returned by the search tool
441
+ usage?.prompt_tokens_details?.cached_tokens ||
442
+ 0;
443
+
444
+ const usageData = {
445
+ sourceId: `llm:${modelName}`,
446
+ input_tokens: inputTokens,
447
+ output_tokens: outputTokens,
448
+ input_tokens_cache_write: 0,
449
+ input_tokens_cache_read: cachedInputTokens,
450
+ cost: usage?.cost || 0, // for web search tool
451
+ keySource: metadata.keySource,
452
+ agentId: metadata.agentId,
453
+ teamId: metadata.teamId,
454
+ };
455
+ SystemEvents.emit('USAGE:LLM', usageData);
456
+
457
+ return usageData;
458
+ }
459
+
460
+ public formatToolsConfig({ type = 'function', toolDefinitions, toolChoice = 'auto' }) {
461
+ let tools: OpenAI.ChatCompletionTool[] = [];
462
+
463
+ if (type === 'function') {
464
+ tools = toolDefinitions.map((tool) => {
465
+ const { name, description, properties, requiredFields } = tool;
466
+
467
+ return {
468
+ type: 'function',
469
+ function: {
470
+ name,
471
+ description,
472
+ parameters: {
473
+ type: 'object',
474
+ properties,
475
+ required: requiredFields,
476
+ },
477
+ },
478
+ };
479
+ });
480
+ }
481
+
482
+ return tools?.length > 0 ? { tools, tool_choice: toolChoice || 'auto' } : {};
483
+ }
484
+
485
+ public transformToolMessageBlocks({
486
+ messageBlock,
487
+ toolsData,
488
+ }: {
489
+ messageBlock: TLLMMessageBlock;
490
+ toolsData: ToolData[];
491
+ }): TLLMToolResultMessageBlock[] {
492
+ const messageBlocks: TLLMToolResultMessageBlock[] = [];
493
+
494
+ if (messageBlock) {
495
+ const transformedMessageBlock = {
496
+ ...messageBlock,
497
+ content: typeof messageBlock.content === 'object' ? JSON.stringify(messageBlock.content) : messageBlock.content,
498
+ };
499
+ if (transformedMessageBlock.tool_calls) {
500
+ for (let toolCall of transformedMessageBlock.tool_calls) {
501
+ toolCall.function.arguments =
502
+ typeof toolCall.function.arguments === 'object' ? JSON.stringify(toolCall.function.arguments) : toolCall.function.arguments;
503
+ }
504
+ }
505
+ messageBlocks.push(transformedMessageBlock);
506
+ }
507
+
508
+ const transformedToolsData = toolsData.map((toolData) => ({
509
+ tool_call_id: toolData.id,
510
+ role: TLLMMessageRole.Tool, // toolData.role as TLLMMessageRole, //should always be 'tool' for OpenAI
511
+ name: toolData.name,
512
+ content: typeof toolData.result === 'string' ? toolData.result : JSON.stringify(toolData.result), // Ensure content is a string
513
+ }));
514
+
515
+ return [...messageBlocks, ...transformedToolsData];
516
+ }
517
+
518
+ public getConsistentMessages(messages) {
519
+ const _messages = LLMHelper.removeDuplicateUserMessages(messages);
520
+
521
+ return _messages.map((message) => {
522
+ const _message = { ...message };
523
+ let textContent = '';
524
+
525
+ if (message?.parts) {
526
+ textContent = message.parts.map((textBlock) => textBlock?.text || '').join(' ');
527
+ } else if (Array.isArray(message?.content)) {
528
+ textContent = message.content.map((textBlock) => textBlock?.text || '').join(' ');
529
+ } else if (message?.content) {
530
+ textContent = message.content;
531
+ }
532
+
533
+ _message.content = textContent;
534
+
535
+ return _message;
536
+ });
537
+ }
538
+
539
+ private getValidImageFiles(files: BinaryInput[]) {
540
+ const validSources = [];
541
+
542
+ for (let file of files) {
543
+ if (this.validImageMimeTypes.includes(file?.mimetype)) {
544
+ validSources.push(file);
545
+ }
546
+ }
547
+
548
+ return validSources;
549
+ }
550
+
551
+ private async getImageData(
552
+ files: BinaryInput[],
553
+ agentId: string
554
+ ): Promise<
555
+ {
556
+ type: string;
557
+ image_url: { url: string };
558
+ }[]
559
+ > {
560
+ try {
561
+ const imageData = [];
562
+
563
+ for (let file of files) {
564
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
565
+ const base64Data = bufferData.toString('base64');
566
+ const url = `data:${file.mimetype};base64,${base64Data}`;
567
+
568
+ imageData.push({
569
+ type: 'image_url',
570
+ image_url: { url },
571
+ });
572
+ }
573
+
574
+ return imageData;
575
+ } catch (error) {
576
+ throw error;
577
+ }
578
+ }
579
+
580
+ private async getDocumentData(
581
+ files: BinaryInput[],
582
+ agentId: string
583
+ ): Promise<
584
+ {
585
+ type: string;
586
+ file: {
587
+ filename: string;
588
+ file_data: string;
589
+ };
590
+ }[]
591
+ > {
592
+ try {
593
+ const documentData = [];
594
+
595
+ // Note: We're embedding the file data in the prompt, but we should ideally be uploading the files to OpenAI first
596
+ // in case we start to support bigger files. Refer to this guide: https://platform.openai.com/docs/guides/pdf-files?api-mode=chat
597
+ for (let file of files) {
598
+ const bufferData = await file.readData(AccessCandidate.agent(agentId));
599
+ const base64Data = bufferData.toString('base64');
600
+ const fileData = `data:${file.mimetype};base64,${base64Data}`;
601
+
602
+ documentData.push({
603
+ type: 'file',
604
+ file: {
605
+ file_data: fileData,
606
+ filename: await file.getName(),
607
+ },
608
+ });
609
+ }
610
+
611
+ return documentData;
612
+ } catch (error) {
613
+ throw error;
614
+ }
615
+ }
616
+
617
+ private getWebSearchTool(params: TLLMParams) {
618
+ const searchCity = params?.webSearchCity;
619
+ const searchCountry = params?.webSearchCountry;
620
+ const searchRegion = params?.webSearchRegion;
621
+ const searchTimezone = params?.webSearchTimezone;
622
+
623
+ const location: {
624
+ type: 'approximate';
625
+ city?: string;
626
+ country?: string;
627
+ region?: string;
628
+ timezone?: string;
629
+ } = {
630
+ type: 'approximate', // Required, always be 'approximate' when we implement location
631
+ };
632
+
633
+ if (searchCity) location.city = searchCity;
634
+ if (searchCountry) location.country = searchCountry;
635
+ if (searchRegion) location.region = searchRegion;
636
+ if (searchTimezone) location.timezone = searchTimezone;
637
+
638
+ const searchTool: {
639
+ type: TSearchTool;
640
+ search_context_size: TSearchContextSize;
641
+ user_location?: TSearchLocation;
642
+ } = {
643
+ type: SEARCH_TOOL.type,
644
+ search_context_size: params?.webSearchContextSize || 'medium',
645
+ };
646
+
647
+ // Add location only if any location field is provided. Since 'type' is always present, we check if the number of keys in the location object is greater than 1.
648
+ if (Object.keys(location).length > 1) {
649
+ searchTool.user_location = location;
650
+ }
651
+
652
+ return searchTool;
653
+ }
654
+
655
+ private async validateTokenLimit({
656
+ acRequest,
657
+ maxTokens,
658
+ promptTokens,
659
+ context,
660
+ }: {
661
+ acRequest: AccessRequest;
662
+ maxTokens: number;
663
+ promptTokens: number;
664
+ context: ILLMRequestContext;
665
+ }): Promise<void> {
666
+ const provider = await this.getProvider(acRequest, context.modelEntryName);
667
+
668
+ await provider.validateTokensLimit({
669
+ model: context.modelEntryName,
670
+ promptTokens,
671
+ completionTokens: maxTokens,
672
+ hasAPIKey: context.isUserKey,
673
+ });
674
+ }
675
+
676
+ private async getProvider(acRequest: AccessRequest, modelEntryName: string) {
677
+ const modelsProviderConnector = ConnectorService.getModelsProviderConnector();
678
+ const modelsProvider = modelsProviderConnector.requester(acRequest.candidate as AccessCandidate);
679
+
680
+ return modelsProvider;
681
+ }
682
+
683
+ private async prepareBodyForWebSearchRequest(params: TLLMParams): Promise<OpenAI.Responses.ResponseCreateParams> {
684
+ const body: OpenAI.Responses.ResponseCreateParams = {
685
+ model: params.model as string,
686
+ input: params.messages,
687
+ stream: true,
688
+ };
689
+
690
+ if (params?.max_output_tokens !== undefined) {
691
+ body.max_output_tokens = params.max_output_tokens;
692
+ }
693
+
694
+ // #region Handle tools configuration
695
+
696
+ const searchTool = this.getWebSearchTool(params);
697
+ body.tools = [searchTool];
698
+
699
+ if (params?.toolsConfig?.tool_choice) {
700
+ body.tool_choice = params.toolsConfig.tool_choice as TOpenAIResponseToolChoice;
701
+ }
702
+
703
+ return body;
704
+ }
705
+
706
+ private async prepareBodyForImageGenRequest(params: TLLMParams): Promise<OpenAI.Images.ImageGenerateParams> {
707
+ const { model, size, quality, n, responseFormat, style } = params;
708
+
709
+ const body: OpenAI.Images.ImageGenerateParams = {
710
+ prompt: params.prompt,
711
+ model: model as string,
712
+ size: size as OpenAI.Images.ImageGenerateParams['size'],
713
+ n: n || 1,
714
+ };
715
+
716
+ if (quality) {
717
+ body.quality = quality;
718
+ }
719
+
720
+ // * Models like 'gpt-image-1' do not support the 'response_format' parameter, so we only set it when explicitly specified.
721
+ if (responseFormat) {
722
+ body.response_format = responseFormat;
723
+ }
724
+
725
+ if (style) {
726
+ body.style = style;
727
+ }
728
+
729
+ return body;
730
+ }
731
+
732
+ private async prepareBodyForImageEditRequest(params: TLLMParams): Promise<OpenAI.Images.ImageEditParams> {
733
+ const { model, size, n, responseFormat } = params;
734
+
735
+ const body: OpenAI.Images.ImageEditParams = {
736
+ prompt: params.prompt,
737
+ model: model as string,
738
+ size: size as OpenAI.Images.ImageEditParams['size'],
739
+ n: n || 1,
740
+ image: null,
741
+ };
742
+
743
+ // * Models like 'gpt-image-1' do not support the 'response_format' parameter, so we only set it when explicitly specified.
744
+ if (responseFormat) {
745
+ body.response_format = responseFormat;
746
+ }
747
+
748
+ const files: BinaryInput[] = params?.files || [];
749
+
750
+ if (files.length > 0) {
751
+ const images = await Promise.all(
752
+ files.map(
753
+ async (file) =>
754
+ await toFile(await file.getReadStream(), await file.getName(), {
755
+ type: file.mimetype,
756
+ })
757
+ )
758
+ );
759
+
760
+ body.image = images;
761
+ }
762
+
763
+ return body;
764
+ }
765
+
766
+ private async prepareBody(params: TLLMParams): Promise<OpenAI.ChatCompletionCreateParams> {
767
+ const messages = await this.prepareMessages(params);
768
+
769
+ //#region Handle JSON response format
770
+ // TODO: We have better parameter to have structured response, need to implement it.
771
+ const responseFormat = params?.responseFormat || '';
772
+ if (responseFormat === 'json') {
773
+ // We assume that the system message is first item in messages array
774
+ if (messages?.[0]?.role === TLLMMessageRole.System) {
775
+ messages[0].content += JSON_RESPONSE_INSTRUCTION;
776
+ } else {
777
+ messages.unshift({ role: TLLMMessageRole.System, content: JSON_RESPONSE_INSTRUCTION });
778
+ }
779
+
780
+ if (MODELS_WITH_JSON_RESPONSE.includes(params.model as string)) {
781
+ params.responseFormat = { type: 'json_object' };
782
+ } else {
783
+ params.responseFormat = undefined; // We need to reset it, otherwise 'json' will be passed as a parameter to the OpenAI API
784
+ }
785
+ }
786
+ //#endregion Handle JSON response format
787
+
788
+ const body: OpenAI.ChatCompletionCreateParams = {
789
+ model: params.model as string,
790
+ messages,
791
+ };
792
+
793
+ if (params?.toolsConfig?.tools && params?.toolsConfig?.tools?.length > 0) {
794
+ body.tools = params?.toolsConfig?.tools as OpenAI.ChatCompletionTool[];
795
+ }
796
+
797
+ if (params?.toolsConfig?.tool_choice) {
798
+ body.tool_choice = params?.toolsConfig?.tool_choice as OpenAI.ChatCompletionToolChoiceOption;
799
+ }
800
+
801
+ if (params?.maxTokens !== undefined) body.max_completion_tokens = params.maxTokens;
802
+ if (params?.temperature !== undefined) body.temperature = params.temperature;
803
+ if (params?.topP !== undefined) body.top_p = params.topP;
804
+ if (params?.frequencyPenalty !== undefined) body.frequency_penalty = params.frequencyPenalty;
805
+ if (params?.presencePenalty !== undefined) body.presence_penalty = params.presencePenalty;
806
+ if (params?.responseFormat !== undefined) body.response_format = params.responseFormat;
807
+ if (params?.stopSequences?.length) body.stop = params.stopSequences;
808
+
809
+ return body;
810
+ }
811
+
812
+ private async prepareMessages(params: TLLMParams) {
813
+ const messages = params?.messages || [];
814
+
815
+ const files: BinaryInput[] = params?.files || []; // Assign file from the original parameters to avoid overwriting the original constructor
816
+
817
+ if (files.length > 0) {
818
+ // #region Upload files
819
+ const promises = [];
820
+ const _files = [];
821
+
822
+ for (let image of files) {
823
+ const binaryInput = BinaryInput.from(image);
824
+ promises.push(binaryInput.upload(AccessCandidate.agent(params.agentId)));
825
+
826
+ _files.push(binaryInput);
827
+ }
828
+
829
+ await Promise.all(promises);
830
+ // #endregion Upload files
831
+
832
+ const validImageFiles = this.getValidImageFiles(_files);
833
+ const validDocumentFiles = this.getValidDocumentFiles(_files);
834
+
835
+ const imageData = validImageFiles.length > 0 ? await this.processImageData(validImageFiles, params.agentId) : [];
836
+ const documentData = validDocumentFiles.length > 0 ? await this.processDocumentData(validDocumentFiles, params.agentId) : [];
837
+
838
+ const userMessage = Array.isArray(messages) ? messages.pop() : {};
839
+ const prompt = userMessage?.content || '';
840
+
841
+ const promptData = [{ type: 'text', text: prompt || '' }, ...imageData, ...documentData];
842
+
843
+ messages.push({ role: 'user', content: promptData });
844
+ }
845
+
846
+ return messages;
847
+ }
848
+ }