@smythos/sre 1.5.75 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -168,6 +168,7 @@ export * from './subsystems/LLMManager/LLM.service/connectors/Bedrock.class';
168
168
  export * from './subsystems/LLMManager/LLM.service/connectors/Echo.class';
169
169
  export * from './subsystems/LLMManager/LLM.service/connectors/GoogleAI.class';
170
170
  export * from './subsystems/LLMManager/LLM.service/connectors/Groq.class';
171
+ export * from './subsystems/LLMManager/LLM.service/connectors/Ollama.class';
171
172
  export * from './subsystems/LLMManager/LLM.service/connectors/Perplexity.class';
172
173
  export * from './subsystems/LLMManager/LLM.service/connectors/VertexAI.class';
173
174
  export * from './subsystems/LLMManager/LLM.service/connectors/xAI.class';
@@ -0,0 +1,45 @@
1
+ import EventEmitter from 'events';
2
+ import { TLLMMessageBlock, ToolData, APIKeySource, ILLMRequestFuncParams, TLLMChatResponse, TLLMPreparedParams, TLLMToolResultMessageBlock, TLLMRequestBody } from '@sre/types/LLM.types';
3
+ import { LLMConnector } from '../LLMConnector';
4
+ export declare class OllamaConnector extends LLMConnector {
5
+ name: string;
6
+ private getClient;
7
+ protected request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse>;
8
+ protected streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter>;
9
+ protected reqBodyAdapter(params: TLLMPreparedParams): Promise<TLLMRequestBody>;
10
+ protected reportUsage(usage: {
11
+ prompt_tokens: number;
12
+ completion_tokens: number;
13
+ total_tokens: number;
14
+ }, metadata: {
15
+ modelEntryName: string;
16
+ keySource: APIKeySource;
17
+ agentId: string;
18
+ teamId: string;
19
+ }): {
20
+ sourceId: string;
21
+ input_tokens: number;
22
+ output_tokens: number;
23
+ input_tokens_cache_write: number;
24
+ input_tokens_cache_read: number;
25
+ keySource: APIKeySource;
26
+ agentId: string;
27
+ teamId: string;
28
+ };
29
+ transformToolMessageBlocks({ messageBlock, toolsData, }: {
30
+ messageBlock: TLLMMessageBlock;
31
+ toolsData: ToolData[];
32
+ }): TLLMToolResultMessageBlock[];
33
+ formatToolsConfig({ type, toolDefinitions, toolChoice }: {
34
+ type?: string;
35
+ toolDefinitions: any;
36
+ toolChoice?: string;
37
+ }): {
38
+ tools: any[];
39
+ tool_choice: string;
40
+ } | {
41
+ tools?: undefined;
42
+ tool_choice?: undefined;
43
+ };
44
+ getConsistentMessages(messages: TLLMMessageBlock[]): TLLMMessageBlock[];
45
+ }
@@ -228,6 +228,7 @@ export declare const BuiltinLLMProviders: {
228
228
  readonly VertexAI: "VertexAI";
229
229
  readonly xAI: "xAI";
230
230
  readonly Perplexity: "Perplexity";
231
+ readonly Ollama: "Ollama";
231
232
  };
232
233
  export type TBuiltinLLMProvider = (typeof BuiltinLLMProviders)[keyof typeof BuiltinLLMProviders];
233
234
  export interface ILLMProviders {
@@ -245,6 +246,7 @@ export declare const TLLMProvider: {
245
246
  readonly VertexAI: "VertexAI";
246
247
  readonly xAI: "xAI";
247
248
  readonly Perplexity: "Perplexity";
249
+ readonly Ollama: "Ollama";
248
250
  };
249
251
  export type TBedrockSettings = {
250
252
  keyIDName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smythos/sre",
3
- "version": "1.5.75",
3
+ "version": "1.6.1",
4
4
  "description": "Smyth Runtime Environment",
5
5
  "author": "Alaa-eddine KADDOURI",
6
6
  "license": "MIT",
@@ -82,6 +82,7 @@
82
82
  "mime": "^4.0.3",
83
83
  "mysql2": "^3.11.3",
84
84
  "oauth-1.0a": "^2.2.6",
85
+ "ollama": "^0.6.0",
85
86
  "openai": "^5.12.2",
86
87
  "p-limit": "^6.1.0",
87
88
  "qs": "^6.13.0",
@@ -150,7 +150,7 @@ export class HuggingFace extends Component {
150
150
  const binaryFile = BinaryInput.from(value, undefined, undefined, AccessCandidate.agent(agentId));
151
151
  // const buffer = await binaryFile.readData(AccessCandidate.agent(agentId));
152
152
  const buffer = await binaryFile.getBuffer();
153
- const blob = new Blob([buffer]);
153
+ const blob = new Blob([buffer as any]);
154
154
  inputs[name] = blob;
155
155
  } catch (error: any) {
156
156
  return { _error: error?.message || JSON.stringify(error), _debug: logger.output };
@@ -311,4 +311,4 @@ export class HuggingFace extends Component {
311
311
  return { _error: `Error from Hugging Face: \n${error?.message || JSON.stringify(error)}`, _debug: logger.output };
312
312
  }
313
313
  }
314
- }
314
+ }
package/src/index.ts CHANGED
@@ -174,6 +174,7 @@ export * from './subsystems/LLMManager/LLM.service/connectors/Bedrock.class';
174
174
  export * from './subsystems/LLMManager/LLM.service/connectors/Echo.class';
175
175
  export * from './subsystems/LLMManager/LLM.service/connectors/GoogleAI.class';
176
176
  export * from './subsystems/LLMManager/LLM.service/connectors/Groq.class';
177
+ export * from './subsystems/LLMManager/LLM.service/connectors/Ollama.class';
177
178
  export * from './subsystems/LLMManager/LLM.service/connectors/Perplexity.class';
178
179
  export * from './subsystems/LLMManager/LLM.service/connectors/VertexAI.class';
179
180
  export * from './subsystems/LLMManager/LLM.service/connectors/xAI.class';
package/src/index.ts.bak CHANGED
@@ -174,6 +174,7 @@ export * from './subsystems/LLMManager/LLM.service/connectors/Bedrock.class';
174
174
  export * from './subsystems/LLMManager/LLM.service/connectors/Echo.class';
175
175
  export * from './subsystems/LLMManager/LLM.service/connectors/GoogleAI.class';
176
176
  export * from './subsystems/LLMManager/LLM.service/connectors/Groq.class';
177
+ export * from './subsystems/LLMManager/LLM.service/connectors/Ollama.class';
177
178
  export * from './subsystems/LLMManager/LLM.service/connectors/Perplexity.class';
178
179
  export * from './subsystems/LLMManager/LLM.service/connectors/VertexAI.class';
179
180
  export * from './subsystems/LLMManager/LLM.service/connectors/xAI.class';
@@ -0,0 +1,362 @@
1
+ import { Ollama, ChatResponse } from 'ollama';
2
+ import EventEmitter from 'events';
3
+
4
+ import { JSON_RESPONSE_INSTRUCTION, BUILT_IN_MODEL_PREFIX } from '@sre/constants';
5
+ import {
6
+ TLLMMessageBlock,
7
+ ToolData,
8
+ TLLMMessageRole,
9
+ APIKeySource,
10
+ TLLMEvent,
11
+ ILLMRequestFuncParams,
12
+ TLLMChatResponse,
13
+ ILLMRequestContext,
14
+ TLLMPreparedParams,
15
+ TLLMToolResultMessageBlock,
16
+ TLLMRequestBody,
17
+ } from '@sre/types/LLM.types';
18
+ import { LLMHelper } from '@sre/LLMManager/LLM.helper';
19
+
20
+ import { LLMConnector } from '../LLMConnector';
21
+ import { SystemEvents } from '@sre/Core/SystemEvents';
22
+ import { Logger } from '@sre/helpers/Log.helper';
23
+
24
+ const logger = Logger('OllamaConnector');
25
+
26
+ type OllamaChatRequest = {
27
+ model: string;
28
+ messages: any[];
29
+ stream?: boolean;
30
+ options?: {
31
+ num_predict?: number;
32
+ temperature?: number;
33
+ top_p?: number;
34
+ top_k?: number;
35
+ stop?: string[];
36
+ };
37
+ tools?: any[];
38
+ };
39
+
40
+ export class OllamaConnector extends LLMConnector {
41
+ public name = 'LLM:Ollama';
42
+
43
+ private getClient(context: ILLMRequestContext): Ollama {
44
+ // Extract baseURL and sanitize it for Ollama SDK
45
+ let host = 'http://localhost:11434';
46
+
47
+ if (context.modelInfo.baseURL) {
48
+ // Handle baseURL that might include /api/ suffix
49
+ const baseURL = context.modelInfo.baseURL;
50
+ if (baseURL.endsWith('/api/')) {
51
+ // Remove /api/ suffix to get the root host
52
+ host = baseURL.replace(/\/api\/$/, '');
53
+ } else if (baseURL.endsWith('/api')) {
54
+ // Remove /api suffix
55
+ host = baseURL.replace(/\/api$/, '');
56
+ } else {
57
+ host = baseURL;
58
+ }
59
+ }
60
+
61
+ // No API key validation required for Ollama (local by default)
62
+ return new Ollama({ host });
63
+ }
64
+
65
+ protected async request({ acRequest, body, context }: ILLMRequestFuncParams): Promise<TLLMChatResponse> {
66
+ try {
67
+ logger.debug(`request ${this.name}`, acRequest.candidate);
68
+ const ollama = this.getClient(context);
69
+
70
+ const result = await ollama.chat({
71
+ ...body,
72
+ stream: false,
73
+ }) as unknown as ChatResponse;
74
+
75
+ const message = result.message;
76
+ const finishReason = result.done_reason || 'stop';
77
+ const usage = {
78
+ prompt_tokens: result.prompt_eval_count || 0,
79
+ completion_tokens: result.eval_count || 0,
80
+ total_tokens: (result.prompt_eval_count || 0) + (result.eval_count || 0),
81
+ };
82
+
83
+ this.reportUsage(usage, {
84
+ modelEntryName: context.modelEntryName,
85
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
86
+ agentId: context.agentId,
87
+ teamId: context.teamId,
88
+ });
89
+
90
+ let toolsData: ToolData[] = [];
91
+ let useTool = false;
92
+
93
+ // Handle tool calls if present
94
+ if (message?.tool_calls) {
95
+ toolsData = message.tool_calls.map((tool, index) => ({
96
+ index,
97
+ id: tool.function?.name || `tool_${index}`,
98
+ type: 'function',
99
+ name: tool.function.name,
100
+ arguments: tool.function.arguments,
101
+ role: TLLMMessageRole.Assistant,
102
+ }));
103
+ useTool = true;
104
+ }
105
+
106
+ return {
107
+ content: message?.content ?? '',
108
+ finishReason,
109
+ useTool,
110
+ toolsData,
111
+ message: message as any,
112
+ usage,
113
+ };
114
+ } catch (error) {
115
+ logger.error(`request ${this.name}`, error, acRequest.candidate);
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ protected async streamRequest({ acRequest, body, context }: ILLMRequestFuncParams): Promise<EventEmitter> {
121
+ try {
122
+ logger.debug(`streamRequest ${this.name}`, acRequest.candidate);
123
+ const emitter = new EventEmitter();
124
+ const usage_data = [];
125
+
126
+ const ollama = this.getClient(context);
127
+ const stream = await ollama.chat({
128
+ ...body,
129
+ stream: true,
130
+ }) as AsyncIterable<ChatResponse>;
131
+
132
+ let toolsData: ToolData[] = [];
133
+ let fullContent = '';
134
+
135
+ (async () => {
136
+ for await (const chunk of stream) {
137
+ // Emit content deltas
138
+ if (chunk.message?.content) {
139
+ const content = chunk.message.content;
140
+ fullContent += content;
141
+ emitter.emit('content', content);
142
+ }
143
+
144
+ // Handle tool calls accumulation
145
+ if (chunk.message?.tool_calls) {
146
+ chunk.message.tool_calls.forEach((toolCall, index) => {
147
+ if (!toolsData[index]) {
148
+ toolsData[index] = {
149
+ index,
150
+ id: toolCall.function?.name || `tool_${index}`,
151
+ type: 'function',
152
+ name: toolCall.function?.name,
153
+ arguments: toolCall.function?.arguments || '',
154
+ role: 'assistant',
155
+ };
156
+ } else {
157
+ // Merge arguments across chunks for string arguments
158
+ if (typeof toolsData[index].arguments === 'string' && typeof toolCall.function?.arguments === 'string') {
159
+ toolsData[index].arguments += toolCall.function.arguments;
160
+ } else {
161
+ // For object arguments, merge them properly
162
+ toolsData[index].arguments = { ...toolsData[index].arguments as any, ...toolCall.function?.arguments };
163
+ }
164
+ }
165
+ });
166
+ }
167
+
168
+ // Capture usage data when available
169
+ if (chunk.prompt_eval_count !== undefined || chunk.eval_count !== undefined) {
170
+ const usage = {
171
+ prompt_tokens: chunk.prompt_eval_count || 0,
172
+ completion_tokens: chunk.eval_count || 0,
173
+ total_tokens: (chunk.prompt_eval_count || 0) + (chunk.eval_count || 0),
174
+ };
175
+ usage_data.push(usage);
176
+ }
177
+ }
178
+
179
+ // Emit tool info if tools were requested
180
+ if (toolsData.length > 0) {
181
+ emitter.emit(TLLMEvent.ToolInfo, toolsData);
182
+ }
183
+
184
+ // Report usage
185
+ usage_data.forEach((usage) => {
186
+ this.reportUsage(usage, {
187
+ modelEntryName: context.modelEntryName,
188
+ keySource: context.isUserKey ? APIKeySource.User : APIKeySource.Smyth,
189
+ agentId: context.agentId,
190
+ teamId: context.teamId,
191
+ });
192
+ });
193
+
194
+ // Final end event
195
+ setTimeout(() => {
196
+ emitter.emit('end', toolsData);
197
+ }, 100);
198
+ })();
199
+
200
+ return emitter;
201
+ } catch (error: any) {
202
+ logger.error(`streamRequest ${this.name}`, error, acRequest.candidate);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ protected async reqBodyAdapter(params: TLLMPreparedParams): Promise<TLLMRequestBody> {
208
+ const messages = params?.messages || [];
209
+
210
+ const body: OllamaChatRequest = {
211
+ model: params.model as string,
212
+ messages,
213
+ };
214
+
215
+ // Handle JSON response format
216
+ const responseFormat = params?.responseFormat || '';
217
+ if (responseFormat === 'json') {
218
+ if (messages?.[0]?.role === 'system') {
219
+ messages[0].content += JSON_RESPONSE_INSTRUCTION;
220
+ } else {
221
+ messages.unshift({ role: 'system', content: JSON_RESPONSE_INSTRUCTION });
222
+ }
223
+ }
224
+
225
+ // Map SRE options to Ollama options
226
+ const options: any = {};
227
+
228
+ if (params.maxTokens !== undefined) options.num_predict = params.maxTokens;
229
+ if (params.temperature !== undefined) options.temperature = params.temperature;
230
+ if (params.topP !== undefined) options.top_p = params.topP;
231
+ if (params.topK !== undefined) options.top_k = params.topK;
232
+ if (params.stopSequences?.length) options.stop = params.stopSequences;
233
+
234
+ if (Object.keys(options).length > 0) {
235
+ body.options = options;
236
+ }
237
+
238
+ // Handle tools
239
+ if (params.toolsConfig?.tools) {
240
+ body.tools = params.toolsConfig.tools.map(tool => ({
241
+ type: 'function',
242
+ function: {
243
+ name: tool.function.name,
244
+ description: tool.function.description,
245
+ parameters: tool.function.parameters,
246
+ },
247
+ }));
248
+ }
249
+
250
+ return body as unknown as TLLMRequestBody;
251
+ }
252
+
253
+ protected reportUsage(
254
+ usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number },
255
+ metadata: { modelEntryName: string; keySource: APIKeySource; agentId: string; teamId: string }
256
+ ) {
257
+ // SmythOS (built-in) models have a prefix, so we need to remove it to get the model name
258
+ const modelName = metadata.modelEntryName.replace(BUILT_IN_MODEL_PREFIX, '');
259
+
260
+ const usageData = {
261
+ sourceId: `llm:${modelName}`,
262
+ input_tokens: usage.prompt_tokens,
263
+ output_tokens: usage.completion_tokens,
264
+ input_tokens_cache_write: 0,
265
+ input_tokens_cache_read: 0,
266
+ keySource: metadata.keySource,
267
+ agentId: metadata.agentId,
268
+ teamId: metadata.teamId,
269
+ };
270
+ SystemEvents.emit('USAGE:LLM', usageData);
271
+
272
+ return usageData;
273
+ }
274
+
275
+ public transformToolMessageBlocks({
276
+ messageBlock,
277
+ toolsData,
278
+ }: {
279
+ messageBlock: TLLMMessageBlock;
280
+ toolsData: ToolData[];
281
+ }): TLLMToolResultMessageBlock[] {
282
+ const messageBlocks: TLLMToolResultMessageBlock[] = [];
283
+
284
+ // Transform the assistant message block if present
285
+ if (messageBlock) {
286
+ const transformedMessageBlock = {
287
+ ...messageBlock,
288
+ content: typeof messageBlock.content === 'object' ? JSON.stringify(messageBlock.content) : messageBlock.content,
289
+ };
290
+ if (transformedMessageBlock.tool_calls) {
291
+ for (let toolCall of transformedMessageBlock.tool_calls) {
292
+ const args = toolCall?.function?.arguments;
293
+ if (typeof args === 'string') {
294
+ try {
295
+ toolCall.function.arguments = JSON.parse(args);
296
+ } catch {
297
+ toolCall.function.arguments = {};
298
+ }
299
+ }
300
+ // If it's already an object, keep as-is for Ollama
301
+ }
302
+ }
303
+ messageBlocks.push(transformedMessageBlock);
304
+ }
305
+
306
+ // Transform tool results into tool role messages
307
+ const transformedToolsData = toolsData.map((toolData) => ({
308
+ tool_call_id: toolData.id,
309
+ role: TLLMMessageRole.Tool,
310
+ name: toolData.name,
311
+ content: typeof toolData.result === 'string' ? toolData.result : JSON.stringify(toolData.result),
312
+ }));
313
+
314
+ return [...messageBlocks, ...transformedToolsData];
315
+ }
316
+
317
+ public formatToolsConfig({ type = 'function', toolDefinitions, toolChoice = 'auto' }) {
318
+ let tools = [];
319
+
320
+ if (type === 'function') {
321
+ tools = toolDefinitions.map((tool) => {
322
+ const { name, description, properties, requiredFields } = tool;
323
+
324
+ return {
325
+ type: 'function',
326
+ function: {
327
+ name,
328
+ description,
329
+ parameters: {
330
+ type: 'object',
331
+ properties,
332
+ required: requiredFields,
333
+ },
334
+ },
335
+ };
336
+ });
337
+ }
338
+
339
+ return tools?.length > 0 ? { tools, tool_choice: toolChoice } : {};
340
+ }
341
+
342
+ public getConsistentMessages(messages: TLLMMessageBlock[]): TLLMMessageBlock[] {
343
+ const _messages = LLMHelper.removeDuplicateUserMessages(messages);
344
+
345
+ return _messages.map((message) => {
346
+ const _message = { ...message };
347
+ let textContent = '';
348
+
349
+ if (message?.parts) {
350
+ textContent = message.parts.map((textBlock) => textBlock?.text || '').join(' ');
351
+ } else if (Array.isArray(message?.content)) {
352
+ textContent = message.content.map((textBlock) => textBlock?.text || '').join(' ');
353
+ } else if (message?.content) {
354
+ textContent = message.content as string;
355
+ }
356
+
357
+ _message.content = textContent;
358
+
359
+ return _message;
360
+ });
361
+ }
362
+ }
@@ -11,6 +11,7 @@ import { BedrockConnector } from './connectors/Bedrock.class';
11
11
  import { VertexAIConnector } from './connectors/VertexAI.class';
12
12
  import { PerplexityConnector } from './connectors/Perplexity.class';
13
13
  import { xAIConnector } from './connectors/xAI.class';
14
+ import { OllamaConnector } from './connectors/Ollama.class';
14
15
 
15
16
  export class LLMService extends ConnectorServiceProvider {
16
17
  public register() {
@@ -25,6 +26,7 @@ export class LLMService extends ConnectorServiceProvider {
25
26
  ConnectorService.register(TConnectorService.LLM, 'VertexAI', VertexAIConnector);
26
27
  ConnectorService.register(TConnectorService.LLM, 'xAI', xAIConnector);
27
28
  ConnectorService.register(TConnectorService.LLM, 'Perplexity', PerplexityConnector);
29
+ ConnectorService.register(TConnectorService.LLM, 'Ollama', OllamaConnector);
28
30
  }
29
31
 
30
32
  public init() {
@@ -40,5 +42,6 @@ export class LLMService extends ConnectorServiceProvider {
40
42
  ConnectorService.init(TConnectorService.LLM, 'VertexAI');
41
43
  ConnectorService.init(TConnectorService.LLM, 'xAI');
42
44
  ConnectorService.init(TConnectorService.LLM, 'Perplexity');
45
+ ConnectorService.init(TConnectorService.LLM, 'Ollama');
43
46
  }
44
47
  }
@@ -129,7 +129,20 @@ export class RAMCache extends CacheConnector {
129
129
  const entry = this.cache.get(fullMetadataKey);
130
130
 
131
131
  if (entry) {
132
- entry.metadata = metadata;
132
+ const existingMetadata = entry.metadata || {};
133
+ const existingAcl = existingMetadata?.acl;
134
+ const mergedMetadata: CacheMetadata = { ...existingMetadata, ...metadata } as CacheMetadata;
135
+
136
+ // Preserve or establish ACL; always ensure requester retains ownership
137
+ if (existingAcl) {
138
+ mergedMetadata.acl = ACL.from(existingAcl).addAccess(acRequest.candidate.role, acRequest.candidate.id, TAccessLevel.Owner).ACL;
139
+ } else if (mergedMetadata.acl) {
140
+ mergedMetadata.acl = ACL.from(mergedMetadata.acl).addAccess(acRequest.candidate.role, acRequest.candidate.id, TAccessLevel.Owner).ACL;
141
+ } else {
142
+ mergedMetadata.acl = new ACL().addAccess(acRequest.candidate.role, acRequest.candidate.id, TAccessLevel.Owner).ACL;
143
+ }
144
+
145
+ entry.metadata = mergedMetadata;
133
146
  this.cache.set(fullMetadataKey, entry);
134
147
  }
135
148
  }
@@ -191,7 +191,7 @@ export class RuntimeContext extends EventEmitter {
191
191
 
192
192
  public enqueueSync() {
193
193
  if (!this.ctxFile) return;
194
- console.log('ENQUEUE SYNC');
194
+
195
195
  this._syncQueue = this._syncQueue
196
196
  .then(() => this.sync())
197
197
  .catch((err) => {
@@ -268,6 +268,7 @@ export const BuiltinLLMProviders = {
268
268
  VertexAI: 'VertexAI',
269
269
  xAI: 'xAI',
270
270
  Perplexity: 'Perplexity',
271
+ Ollama: 'Ollama',
271
272
  } as const;
272
273
  // Base provider type
273
274
  export type TBuiltinLLMProvider = (typeof BuiltinLLMProviders)[keyof typeof BuiltinLLMProviders];